diff --git a/proto/api/v1/workspace_service.proto b/proto/api/v1/workspace_service.proto index f9141ffa1..012bc5077 100644 --- a/proto/api/v1/workspace_service.proto +++ b/proto/api/v1/workspace_service.proto @@ -72,25 +72,28 @@ message WorkspaceSetting { } message WorkspaceGeneralSetting { + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + string theme = 1; // disallow_user_registration disallows user registration. - bool disallow_user_registration = 1; + bool disallow_user_registration = 2; // disallow_password_auth disallows password authentication. - bool disallow_password_auth = 2; + bool disallow_password_auth = 3; // additional_script is the additional script. - string additional_script = 3; + string additional_script = 4; // additional_style is the additional style. - string additional_style = 4; + string additional_style = 5; // custom_profile is the custom profile. - WorkspaceCustomProfile custom_profile = 5; + WorkspaceCustomProfile custom_profile = 6; // week_start_day_offset is the week start day offset from Sunday. // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday // Default is Sunday. - int32 week_start_day_offset = 6; + int32 week_start_day_offset = 7; // disallow_change_username disallows changing username. - bool disallow_change_username = 7; + bool disallow_change_username = 8; // disallow_change_nickname disallows changing nickname. - bool disallow_change_nickname = 8; + bool disallow_change_nickname = 9; } message WorkspaceCustomProfile { diff --git a/proto/gen/api/v1/workspace_service.pb.go b/proto/gen/api/v1/workspace_service.pb.go index 69530c857..96c5084f3 100644 --- a/proto/gen/api/v1/workspace_service.pb.go +++ b/proto/gen/api/v1/workspace_service.pb.go @@ -300,24 +300,27 @@ func (*WorkspaceSetting_MemoRelatedSetting) isWorkspaceSetting_Value() {} type WorkspaceGeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` // disallow_user_registration disallows user registration. - DisallowUserRegistration bool `protobuf:"varint,1,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` + DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` // disallow_password_auth disallows password authentication. - DisallowPasswordAuth bool `protobuf:"varint,2,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` + DisallowPasswordAuth bool `protobuf:"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` // additional_script is the additional script. - AdditionalScript string `protobuf:"bytes,3,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` + AdditionalScript string `protobuf:"bytes,4,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` // additional_style is the additional style. - AdditionalStyle string `protobuf:"bytes,4,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` + AdditionalStyle string `protobuf:"bytes,5,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` // custom_profile is the custom profile. - CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,5,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` + CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,6,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` // week_start_day_offset is the week start day offset from Sunday. // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday // Default is Sunday. - WeekStartDayOffset int32 `protobuf:"varint,6,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` + WeekStartDayOffset int32 `protobuf:"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` // disallow_change_username disallows changing username. - DisallowChangeUsername bool `protobuf:"varint,7,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` + DisallowChangeUsername bool `protobuf:"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` // disallow_change_nickname disallows changing nickname. - DisallowChangeNickname bool `protobuf:"varint,8,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` + DisallowChangeNickname bool `protobuf:"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -352,6 +355,13 @@ func (*WorkspaceGeneralSetting) Descriptor() ([]byte, []int) { return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{3} } +func (x *WorkspaceGeneralSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + func (x *WorkspaceGeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration @@ -887,16 +897,17 @@ const file_api_v1_workspace_service_proto_rawDesc = "" + "\x0fstorage_setting\x18\x03 \x01(\v2%.memos.api.v1.WorkspaceStorageSettingH\x00R\x0estorageSetting\x12]\n" + "\x14memo_related_setting\x18\x04 \x01(\v2).memos.api.v1.WorkspaceMemoRelatedSettingH\x00R\x12memoRelatedSetting:f\xeaAc\n" + "\x1eapi.memos.dev/WorkspaceSetting\x12\x1cworkspace/settings/{setting}*\x11workspaceSettings2\x10workspaceSettingB\a\n" + - "\x05value\"\xd9\x03\n" + - "\x17WorkspaceGeneralSetting\x12<\n" + - "\x1adisallow_user_registration\x18\x01 \x01(\bR\x18disallowUserRegistration\x124\n" + - "\x16disallow_password_auth\x18\x02 \x01(\bR\x14disallowPasswordAuth\x12+\n" + - "\x11additional_script\x18\x03 \x01(\tR\x10additionalScript\x12)\n" + - "\x10additional_style\x18\x04 \x01(\tR\x0fadditionalStyle\x12K\n" + - "\x0ecustom_profile\x18\x05 \x01(\v2$.memos.api.v1.WorkspaceCustomProfileR\rcustomProfile\x121\n" + - "\x15week_start_day_offset\x18\x06 \x01(\x05R\x12weekStartDayOffset\x128\n" + - "\x18disallow_change_username\x18\a \x01(\bR\x16disallowChangeUsername\x128\n" + - "\x18disallow_change_nickname\x18\b \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + + "\x05value\"\xef\x03\n" + + "\x17WorkspaceGeneralSetting\x12\x14\n" + + "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + + "\x10additional_style\x18\x05 \x01(\tR\x0fadditionalStyle\x12K\n" + + "\x0ecustom_profile\x18\x06 \x01(\v2$.memos.api.v1.WorkspaceCustomProfileR\rcustomProfile\x121\n" + + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + "\x16WorkspaceCustomProfile\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml index a682accfc..2b35bab33 100644 --- a/proto/gen/apidocs.swagger.yaml +++ b/proto/gen/apidocs.swagger.yaml @@ -2900,6 +2900,11 @@ definitions: apiv1WorkspaceGeneralSetting: type: object properties: + theme: + type: string + description: |- + theme is the name of the selected theme. + This references a CSS file in the web/public/themes/ directory. disallowUserRegistration: type: boolean description: disallow_user_registration disallows user registration. diff --git a/proto/gen/store/workspace_setting.pb.go b/proto/gen/store/workspace_setting.pb.go index 9e4951ac3..ac097b341 100644 --- a/proto/gen/store/workspace_setting.pb.go +++ b/proto/gen/store/workspace_setting.pb.go @@ -313,24 +313,27 @@ func (x *WorkspaceBasicSetting) GetSchemaVersion() string { type WorkspaceGeneralSetting struct { state protoimpl.MessageState `protogen:"open.v1"` + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` // disallow_user_registration disallows user registration. - DisallowUserRegistration bool `protobuf:"varint,1,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` + DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` // disallow_password_auth disallows password authentication. - DisallowPasswordAuth bool `protobuf:"varint,2,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` + DisallowPasswordAuth bool `protobuf:"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` // additional_script is the additional script. - AdditionalScript string `protobuf:"bytes,3,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` + AdditionalScript string `protobuf:"bytes,4,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` // additional_style is the additional style. - AdditionalStyle string `protobuf:"bytes,4,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` + AdditionalStyle string `protobuf:"bytes,5,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` // custom_profile is the custom profile. - CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,5,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` + CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,6,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` // week_start_day_offset is the week start day offset from Sunday. // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday // Default is Sunday. - WeekStartDayOffset int32 `protobuf:"varint,6,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` + WeekStartDayOffset int32 `protobuf:"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` // disallow_change_username disallows changing username. - DisallowChangeUsername bool `protobuf:"varint,7,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` + DisallowChangeUsername bool `protobuf:"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` // disallow_change_nickname disallows changing nickname. - DisallowChangeNickname bool `protobuf:"varint,8,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` + DisallowChangeNickname bool `protobuf:"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -365,6 +368,13 @@ func (*WorkspaceGeneralSetting) Descriptor() ([]byte, []int) { return file_store_workspace_setting_proto_rawDescGZIP(), []int{2} } +func (x *WorkspaceGeneralSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + func (x *WorkspaceGeneralSetting) GetDisallowUserRegistration() bool { if x != nil { return x.DisallowUserRegistration @@ -796,16 +806,17 @@ const file_store_workspace_setting_proto_rawDesc = "" + "\x15WorkspaceBasicSetting\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12%\n" + - "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xd8\x03\n" + - "\x17WorkspaceGeneralSetting\x12<\n" + - "\x1adisallow_user_registration\x18\x01 \x01(\bR\x18disallowUserRegistration\x124\n" + - "\x16disallow_password_auth\x18\x02 \x01(\bR\x14disallowPasswordAuth\x12+\n" + - "\x11additional_script\x18\x03 \x01(\tR\x10additionalScript\x12)\n" + - "\x10additional_style\x18\x04 \x01(\tR\x0fadditionalStyle\x12J\n" + - "\x0ecustom_profile\x18\x05 \x01(\v2#.memos.store.WorkspaceCustomProfileR\rcustomProfile\x121\n" + - "\x15week_start_day_offset\x18\x06 \x01(\x05R\x12weekStartDayOffset\x128\n" + - "\x18disallow_change_username\x18\a \x01(\bR\x16disallowChangeUsername\x128\n" + - "\x18disallow_change_nickname\x18\b \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xee\x03\n" + + "\x17WorkspaceGeneralSetting\x12\x14\n" + + "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + + "\x10additional_style\x18\x05 \x01(\tR\x0fadditionalStyle\x12J\n" + + "\x0ecustom_profile\x18\x06 \x01(\v2#.memos.store.WorkspaceCustomProfileR\rcustomProfile\x121\n" + + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + "\x16WorkspaceCustomProfile\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + diff --git a/proto/store/workspace_setting.proto b/proto/store/workspace_setting.proto index 9d2f3b442..73ea8fa0c 100644 --- a/proto/store/workspace_setting.proto +++ b/proto/store/workspace_setting.proto @@ -34,25 +34,27 @@ message WorkspaceBasicSetting { } message WorkspaceGeneralSetting { + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + string theme = 1; // disallow_user_registration disallows user registration. - bool disallow_user_registration = 1; + bool disallow_user_registration = 2; // disallow_password_auth disallows password authentication. - bool disallow_password_auth = 2; + bool disallow_password_auth = 3; // additional_script is the additional script. - string additional_script = 3; + string additional_script = 4; // additional_style is the additional style. - string additional_style = 4; + string additional_style = 5; // custom_profile is the custom profile. - WorkspaceCustomProfile custom_profile = 5; + WorkspaceCustomProfile custom_profile = 6; // week_start_day_offset is the week start day offset from Sunday. // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday // Default is Sunday. - int32 week_start_day_offset = 6; - + int32 week_start_day_offset = 7; // disallow_change_username disallows changing username. - bool disallow_change_username = 7; + bool disallow_change_username = 8; // disallow_change_nickname disallows changing nickname. - bool disallow_change_nickname = 8; + bool disallow_change_nickname = 9; } message WorkspaceCustomProfile { diff --git a/server/router/api/v1/workspace_service.go b/server/router/api/v1/workspace_service.go index 614682cd7..2ca0a8d47 100644 --- a/server/router/api/v1/workspace_service.go +++ b/server/router/api/v1/workspace_service.go @@ -150,6 +150,7 @@ func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSe return nil } generalSetting := &v1pb.WorkspaceGeneralSetting{ + Theme: setting.Theme, DisallowUserRegistration: setting.DisallowUserRegistration, DisallowPasswordAuth: setting.DisallowPasswordAuth, AdditionalScript: setting.AdditionalScript, @@ -175,6 +176,7 @@ func convertWorkspaceGeneralSettingToStore(setting *v1pb.WorkspaceGeneralSetting return nil } generalSetting := &storepb.WorkspaceGeneralSetting{ + Theme: setting.Theme, DisallowUserRegistration: setting.DisallowUserRegistration, DisallowPasswordAuth: setting.DisallowPasswordAuth, AdditionalScript: setting.AdditionalScript, diff --git a/web/components.json b/web/components.json index a1de0a8db..2082f482a 100644 --- a/web/components.json +++ b/web/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/style.css", + "css": "src/index.css", "baseColor": "zinc", "cssVariables": true, "prefix": "" diff --git a/web/src/App.tsx b/web/src/App.tsx index 5f3547a13..0e1bf90d8 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -5,6 +5,7 @@ import { Outlet } from "react-router-dom"; import { getSystemColorScheme } from "./helpers/utils"; import useNavigateTo from "./hooks/useNavigateTo"; import { userStore, workspaceStore } from "./store/v2"; +import { loadTheme } from "./utils/theme"; const App = observer(() => { const { i18n } = useTranslation(); @@ -103,6 +104,11 @@ const App = observer(() => { }); }, [userSetting?.locale, userSetting?.appearance]); + // Load theme when workspace setting changes, validate API response + useEffect(() => { + loadTheme(workspaceGeneralSetting.theme); + }, [workspaceGeneralSetting.theme]); + return ; }); diff --git a/web/src/components/Settings/WorkspaceSection.tsx b/web/src/components/Settings/WorkspaceSection.tsx index 22fa7c8a9..6bdf0f80b 100644 --- a/web/src/components/Settings/WorkspaceSection.tsx +++ b/web/src/components/Settings/WorkspaceSection.tsx @@ -17,6 +17,7 @@ import { WorkspaceSettingKey } from "@/store/v2/workspace"; import { IdentityProvider } from "@/types/proto/api/v1/idp_service"; import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_service"; import { useTranslate } from "@/utils/i18n"; +import ThemeSelector from "../ThemeSelector"; import UpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; const WorkspaceSection = observer(() => { @@ -82,6 +83,14 @@ const WorkspaceSection = observer(() => {

{t("setting.system-section.title")}

+
+ Theme + updatePartialSetting({ theme: value })} + className="min-w-fit" + /> +
{t("setting.system-section.additional-style")}
diff --git a/web/src/components/ThemeSelector.tsx b/web/src/components/ThemeSelector.tsx new file mode 100644 index 000000000..6f863fdb4 --- /dev/null +++ b/web/src/components/ThemeSelector.tsx @@ -0,0 +1,31 @@ +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface ThemeSelectorProps { + value?: string; + onValueChange: (value: string) => void; + className?: string; +} + +const THEMES = [ + { value: "default", label: "Default" }, + { value: "paper", label: "Paper" }, +] as const; + +export const ThemeSelector = ({ value = "default", onValueChange, className }: ThemeSelectorProps) => { + return ( + + ); +}; + +export default ThemeSelector; diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx index bae8522fa..8be5ffa51 100644 --- a/web/src/components/ui/dropdown-menu.tsx +++ b/web/src/components/ui/dropdown-menu.tsx @@ -6,7 +6,8 @@ import { cn } from "@/lib/utils"; const DropdownMenu = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars +>(({ ...props }, _ref) => { return ; }); DropdownMenu.displayName = "DropdownMenu"; diff --git a/web/src/components/ui/popover.tsx b/web/src/components/ui/popover.tsx index 2c0ca459a..a65233f46 100644 --- a/web/src/components/ui/popover.tsx +++ b/web/src/components/ui/popover.tsx @@ -5,7 +5,8 @@ import { cn } from "@/lib/utils"; const Popover = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars +>(({ ...props }, _ref) => { return ; }); Popover.displayName = "Popover"; diff --git a/web/src/components/ui/sheet.tsx b/web/src/components/ui/sheet.tsx index 1e30cbbc7..4d98bc41d 100644 --- a/web/src/components/ui/sheet.tsx +++ b/web/src/components/ui/sheet.tsx @@ -4,7 +4,8 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const Sheet = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ ...props }, _ref) => { return ; }, ); diff --git a/web/src/components/ui/tooltip.tsx b/web/src/components/ui/tooltip.tsx index e75d2b58e..85578e17c 100644 --- a/web/src/components/ui/tooltip.tsx +++ b/web/src/components/ui/tooltip.tsx @@ -5,7 +5,8 @@ import { cn } from "@/lib/utils"; const TooltipProvider = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ delayDuration = 0, ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars +>(({ delayDuration = 0, ...props }, _ref) => { return ; }); TooltipProvider.displayName = "TooltipProvider"; @@ -13,7 +14,8 @@ TooltipProvider.displayName = "TooltipProvider"; const Tooltip = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars +>(({ ...props }, _ref) => { return ( diff --git a/web/src/index.css b/web/src/index.css index 7571624de..eb777ebcc 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,6 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; -@import "./style.css"; +@import "./themes/default.css"; @custom-variant dark (&: is(.dark *)); diff --git a/web/src/theme/index.ts b/web/src/theme/index.ts deleted file mode 100644 index 77b70ca57..000000000 --- a/web/src/theme/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Theme configuration for Tailwind CSS v4 -// This file is kept for compatibility but no longer used for MUI theme configuration -// All styling is now handled by Tailwind CSS - -export default {}; diff --git a/web/src/style.css b/web/src/themes/default.css similarity index 100% rename from web/src/style.css rename to web/src/themes/default.css diff --git a/web/src/themes/paper.css b/web/src/themes/paper.css new file mode 100644 index 000000000..199554943 --- /dev/null +++ b/web/src/themes/paper.css @@ -0,0 +1,152 @@ +:root { + --background: oklch(0.9765 0.0086 85.8732); + --foreground: oklch(0.2147 0.0157 68.4253); + --card: oklch(0.9765 0.0086 85.8732); + --card-foreground: oklch(0.1725 0.0098 72.3456); + --popover: oklch(0.9882 0.0049 81.2547); + --popover-foreground: oklch(0.2058 0.0127 70.1823); + --primary: oklch(0.4627 0.0471 52.3674); + --primary-foreground: oklch(0.9765 0.0086 85.8732); + --secondary: oklch(0.9412 0.0196 78.9456); + --secondary-foreground: oklch(0.3725 0.0235 65.7412); + --muted: oklch(0.9294 0.0216 82.1567); + --muted-foreground: oklch(0.5294 0.0157 69.8745); + --accent: oklch(0.9412 0.0196 78.9456); + --accent-foreground: oklch(0.2058 0.0127 70.1823); + --destructive: oklch(0.4118 0.0392 25.6734); + --destructive-foreground: oklch(0.9647 0.0078 88.2341); + --border: oklch(0.8824 0.0157 79.3627); + --input: oklch(0.8235 0.0196 82.1456); + --ring: oklch(0.4627 0.0471 52.3674); + --chart-1: oklch(0.5686 0.0549 42.7834); + --chart-2: oklch(0.6275 0.0392 85.6723); + --chart-3: oklch(0.7843 0.0235 78.9456); + --chart-4: oklch(0.7451 0.0314 68.2341); + --chart-5: oklch(0.5412 0.0431 55.8934); + --sidebar: oklch(0.9647 0.0118 83.7892); + --sidebar-foreground: oklch(0.2941 0.0196 67.6524); + --sidebar-primary: oklch(0.4627 0.0471 52.3674); + --sidebar-primary-foreground: oklch(0.9765 0.0086 85.8732); + --sidebar-accent: oklch(0.9412 0.0196 78.9456); + --sidebar-accent-foreground: oklch(0.2647 0.0157 71.2341); + --sidebar-border: oklch(0.9294 0.0137 80.5674); + --sidebar-ring: oklch(0.7647 0.0235 75.8934); + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --radius: 0.5rem; + --shadow-2xs: 0 1px 3px 0px hsl(34 12% 15% / 0.04); + --shadow-xs: 0 1px 3px 0px hsl(34 12% 15% / 0.04); + --shadow-sm: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 1px 2px -1px hsl(34 12% 15% / 0.08); + --shadow: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 1px 2px -1px hsl(34 12% 15% / 0.08); + --shadow-md: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 2px 4px -1px hsl(34 12% 15% / 0.08); + --shadow-lg: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 4px 6px -1px hsl(34 12% 15% / 0.08); + --shadow-xl: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 8px 10px -1px hsl(34 12% 15% / 0.08); + --shadow-2xl: 0 1px 3px 0px hsl(34 12% 15% / 0.18); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark { + --background: oklch(0.1608 0.0118 68.4253); + --foreground: oklch(0.8627 0.0196 82.1456); + --card: oklch(0.1608 0.0118 68.4253); + --card-foreground: oklch(0.8824 0.0157 84.5674); + --popover: oklch(0.2118 0.0137 71.2341); + --popover-foreground: oklch(0.8706 0.0176 83.8934); + --primary: oklch(0.6863 0.0392 52.3674); + --primary-foreground: oklch(0.1608 0.0118 68.4253); + --secondary: oklch(0.2745 0.0157 72.8934); + --secondary-foreground: oklch(0.8627 0.0196 82.1456); + --muted: oklch(0.1961 0.0098 69.7823); + --muted-foreground: oklch(0.6471 0.0196 78.9456); + --accent: oklch(0.2549 0.0118 70.5674); + --accent-foreground: oklch(0.8627 0.0196 82.1456); + --destructive: oklch(0.5294 0.0314 25.6734); + --destructive-foreground: oklch(0.9647 0.0078 88.2341); + --border: oklch(0.3137 0.0157 71.8928); + --input: oklch(0.3725 0.0176 73.2195); + --ring: oklch(0.6863 0.0392 52.3674); + --chart-1: oklch(0.5686 0.0549 42.7834); + --chart-2: oklch(0.6275 0.0392 85.6723); + --chart-3: oklch(0.4706 0.0196 75.2341); + --chart-4: oklch(0.3529 0.0235 71.8934); + --chart-5: oklch(0.5412 0.0431 55.8934); + --sidebar: oklch(0.1412 0.0098 66.7077); + --sidebar-foreground: oklch(0.8627 0.0196 82.1456); + --sidebar-primary: oklch(0.6863 0.0392 52.3674); + --sidebar-primary-foreground: oklch(0.1608 0.0118 68.4253); + --sidebar-accent: oklch(0.2353 0.0118 69.8934); + --sidebar-accent-foreground: oklch(0.8627 0.0196 82.1456); + --sidebar-border: oklch(0.3137 0.0157 71.8928); + --sidebar-ring: oklch(0.6863 0.0392 52.3674); + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --radius: 0.5rem; + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1); + --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1); + --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); +} \ No newline at end of file diff --git a/web/src/types/proto/api/v1/workspace_service.ts b/web/src/types/proto/api/v1/workspace_service.ts index 81056249c..b14ef49ef 100644 --- a/web/src/types/proto/api/v1/workspace_service.ts +++ b/web/src/types/proto/api/v1/workspace_service.ts @@ -42,6 +42,11 @@ export interface WorkspaceSetting { } export interface WorkspaceGeneralSetting { + /** + * theme is the name of the selected theme. + * This references a CSS file in the web/public/themes/ directory. + */ + theme: string; /** disallow_user_registration disallows user registration. */ disallowUserRegistration: boolean; /** disallow_password_auth disallows password authentication. */ @@ -394,6 +399,7 @@ export const WorkspaceSetting: MessageFns = { function createBaseWorkspaceGeneralSetting(): WorkspaceGeneralSetting { return { + theme: "", disallowUserRegistration: false, disallowPasswordAuth: false, additionalScript: "", @@ -407,29 +413,32 @@ function createBaseWorkspaceGeneralSetting(): WorkspaceGeneralSetting { export const WorkspaceGeneralSetting: MessageFns = { encode(message: WorkspaceGeneralSetting, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.theme !== "") { + writer.uint32(10).string(message.theme); + } if (message.disallowUserRegistration !== false) { - writer.uint32(8).bool(message.disallowUserRegistration); + writer.uint32(16).bool(message.disallowUserRegistration); } if (message.disallowPasswordAuth !== false) { - writer.uint32(16).bool(message.disallowPasswordAuth); + writer.uint32(24).bool(message.disallowPasswordAuth); } if (message.additionalScript !== "") { - writer.uint32(26).string(message.additionalScript); + writer.uint32(34).string(message.additionalScript); } if (message.additionalStyle !== "") { - writer.uint32(34).string(message.additionalStyle); + writer.uint32(42).string(message.additionalStyle); } if (message.customProfile !== undefined) { - WorkspaceCustomProfile.encode(message.customProfile, writer.uint32(42).fork()).join(); + WorkspaceCustomProfile.encode(message.customProfile, writer.uint32(50).fork()).join(); } if (message.weekStartDayOffset !== 0) { - writer.uint32(48).int32(message.weekStartDayOffset); + writer.uint32(56).int32(message.weekStartDayOffset); } if (message.disallowChangeUsername !== false) { - writer.uint32(56).bool(message.disallowChangeUsername); + writer.uint32(64).bool(message.disallowChangeUsername); } if (message.disallowChangeNickname !== false) { - writer.uint32(64).bool(message.disallowChangeNickname); + writer.uint32(72).bool(message.disallowChangeNickname); } return writer; }, @@ -442,11 +451,11 @@ export const WorkspaceGeneralSetting: MessageFns = { const tag = reader.uint32(); switch (tag >>> 3) { case 1: { - if (tag !== 8) { + if (tag !== 10) { break; } - message.disallowUserRegistration = reader.bool(); + message.theme = reader.string(); continue; } case 2: { @@ -454,15 +463,15 @@ export const WorkspaceGeneralSetting: MessageFns = { break; } - message.disallowPasswordAuth = reader.bool(); + message.disallowUserRegistration = reader.bool(); continue; } case 3: { - if (tag !== 26) { + if (tag !== 24) { break; } - message.additionalScript = reader.string(); + message.disallowPasswordAuth = reader.bool(); continue; } case 4: { @@ -470,7 +479,7 @@ export const WorkspaceGeneralSetting: MessageFns = { break; } - message.additionalStyle = reader.string(); + message.additionalScript = reader.string(); continue; } case 5: { @@ -478,15 +487,15 @@ export const WorkspaceGeneralSetting: MessageFns = { break; } - message.customProfile = WorkspaceCustomProfile.decode(reader, reader.uint32()); + message.additionalStyle = reader.string(); continue; } case 6: { - if (tag !== 48) { + if (tag !== 50) { break; } - message.weekStartDayOffset = reader.int32(); + message.customProfile = WorkspaceCustomProfile.decode(reader, reader.uint32()); continue; } case 7: { @@ -494,7 +503,7 @@ export const WorkspaceGeneralSetting: MessageFns = { break; } - message.disallowChangeUsername = reader.bool(); + message.weekStartDayOffset = reader.int32(); continue; } case 8: { @@ -502,6 +511,14 @@ export const WorkspaceGeneralSetting: MessageFns = { break; } + message.disallowChangeUsername = reader.bool(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + message.disallowChangeNickname = reader.bool(); continue; } @@ -519,6 +536,7 @@ export const WorkspaceGeneralSetting: MessageFns = { }, fromPartial(object: DeepPartial): WorkspaceGeneralSetting { const message = createBaseWorkspaceGeneralSetting(); + message.theme = object.theme ?? ""; message.disallowUserRegistration = object.disallowUserRegistration ?? false; message.disallowPasswordAuth = object.disallowPasswordAuth ?? false; message.additionalScript = object.additionalScript ?? ""; diff --git a/web/src/utils/theme.ts b/web/src/utils/theme.ts new file mode 100644 index 000000000..e0325ea2d --- /dev/null +++ b/web/src/utils/theme.ts @@ -0,0 +1,42 @@ +import paperThemeContent from "../themes/paper.css?raw"; + +const VALID_THEMES = ["default", "paper"] as const; +type ValidTheme = (typeof VALID_THEMES)[number]; + +const THEME_CONTENT: Record = { + default: null, + paper: paperThemeContent, +}; + +const validateTheme = (theme: string): ValidTheme => { + return VALID_THEMES.includes(theme as ValidTheme) ? (theme as ValidTheme) : "default"; +}; + +export const getStoredTheme = (): ValidTheme => { + const stored = localStorage.getItem("workspace-theme"); + return stored ? validateTheme(stored) : "default"; +}; + +export const loadTheme = (themeName: string): void => { + const validTheme = validateTheme(themeName); + + // Store theme + localStorage.setItem("workspace-theme", validTheme); + + // Remove existing theme + document.getElementById("workspace-theme")?.remove(); + + // Apply theme (skip for default) + if (validTheme !== "default") { + const css = THEME_CONTENT[validTheme]; + if (css) { + const style = document.createElement("style"); + style.id = "workspace-theme"; + style.textContent = css; + document.head.appendChild(style); + } + } + + // Set data attribute + document.documentElement.setAttribute("data-theme", validTheme); +};