S3.1: pixel design language: palette with day-part and season rotation, Fusion Pixel font on whole screen pixels, dither ground, 9-slice dialogs, pixel buttons, menu arrow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d422b2ce69
commit
86dbb4efa1
24 files changed
+777
-145
No files matched your search
@@ -21,7 +21,7 @@ export const organizationContracts = {
|
||||
goalTitle: z.string(), goalDescription: z.string(), providerKey: z.string(), providerSecret: z.string(), providerModel: z.string(),
|
||||
}) satisfies z.ZodType<C.BootstrapOnboardingInput>,
|
||||
setActiveOrganization: z.strictObject({ orgId: id }) satisfies z.ZodType<C.SetActiveOrganizationInput>,
|
||||
setUiPreferences: z.strictObject({ theme: z.enum(["system", "light", "dark"]).optional(), language: z.enum(["en", "zh"]).optional() }) satisfies z.ZodType<C.SetUiPreferencesInput>,
|
||||
setUiPreferences: z.strictObject({ theme: z.enum(["system", "light", "dark", "clock"]).optional(), language: z.enum(["en", "zh"]).optional() }) satisfies z.ZodType<C.SetUiPreferencesInput>,
|
||||
deleteOrganization: z.strictObject({ orgId: id }) satisfies z.ZodType<C.DeleteOrganizationInput>,
|
||||
createOrganization: z.strictObject({
|
||||
organizationName: z.string(), description: z.string().optional(), mission: z.string(), goal: z.string(), employeeName: z.string(),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function setAutonomousPaused(db: DatabaseSync, paused: boolean): void {
|
||||
}
|
||||
|
||||
/** The app-wide theme and language, kept for every company. */
|
||||
export function updateUiPreferences(db: DatabaseSync, input: { theme?: "system" | "light" | "dark"; language?: "en" | "zh" }): void {
|
||||
export function updateUiPreferences(db: DatabaseSync, input: { theme?: "system" | "light" | "dark" | "clock"; language?: "en" | "zh" }): void {
|
||||
const current = db.prepare("select theme, language from app_state where id = 1")
|
||||
.get() as { theme: string; language: string } | undefined;
|
||||
const theme = input.theme ?? current?.theme ?? "system";
|
||||
|
||||
@@ -67,7 +67,7 @@ export class OrganizationService {
|
||||
setAutonomousPaused(this.db, paused);
|
||||
}
|
||||
|
||||
updateUiPreferences(input: { theme?: "system" | "light" | "dark"; language?: "en" | "zh" }): void {
|
||||
updateUiPreferences(input: { theme?: "system" | "light" | "dark" | "clock"; language?: "en" | "zh" }): void {
|
||||
updateUiPreferences(this.db, input);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { usePaletteClock } from "../shared/palette-clock";
|
||||
import { usePixelScale } from "../shared/pixel-scale";
|
||||
import { Modal } from "../shared/Modal";
|
||||
import { StaleBanner } from "./StaleBanner";
|
||||
import { oneOf, readViewState, seedViewState, writeViewState } from "../shared/view-state";
|
||||
@@ -8,7 +10,7 @@ import { hasPageUnsaved } from "../shared/unsaved";
|
||||
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
||||
import { StartupScreen } from "./StartupScreen";
|
||||
import type { Focus } from "../pages/types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Shell } from "../shared/Shell";
|
||||
import { ROUTE_IDS, type RouteId } from "../shared/routes";
|
||||
import { AvatarDirectoryContext, type AvatarLook } from "../shared/ui";
|
||||
@@ -67,18 +69,8 @@ export function App() {
|
||||
}, [snapshot?.agents, snapshot?.employees, snapshot?.activeOrgId]);
|
||||
const [creatingOrg, setCreatingOrg] = useState(false);
|
||||
|
||||
const theme = snapshot?.uiPreferences.theme ?? "system";
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const apply = () => {
|
||||
const resolved = theme === "system" ? (media.matches ? "dark" : "light") : theme;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
};
|
||||
apply();
|
||||
if (theme !== "system") return;
|
||||
media.addEventListener("change", apply);
|
||||
return () => media.removeEventListener("change", apply);
|
||||
}, [theme]);
|
||||
usePaletteClock(snapshot?.uiPreferences.theme ?? "system");
|
||||
usePixelScale();
|
||||
|
||||
if (!snapshot) {
|
||||
// A failed manual retry is already recorded in syncFailure, which the screen shows.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
Fusion Pixel Font
|
||||
https://github.com/TakWolf/fusion-pixel-font
|
||||
|
||||
Copyright (c) 2022, TakWolf (https://takwolf.com).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
Ark Pixel Font
|
||||
https://github.com/TakWolf/ark-pixel-font
|
||||
|
||||
Copyright (c) 2021, TakWolf (https://takwolf.com).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,108 @@
|
||||
[Cubic 11]
|
||||
These fonts are free software.
|
||||
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
|
||||
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
|
||||
此字型是免費的。
|
||||
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
|
||||
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
|
||||
[JF Dot M+H 12]
|
||||
Copyright(c) 2005 M+ FONTS PROJECT
|
||||
[M+ BITMAP FONTS]
|
||||
Copyright (C) 2002-2004 COZ
|
||||
These fonts are free software.
|
||||
Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
|
||||
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
|
||||
これらのフォントはフリー(自由な)ソフトウエアです。
|
||||
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2019–2025 Lee Minseo (quiple@quiple.dev)
|
||||
|
||||
This font software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -601,6 +601,7 @@ export const zh: Record<string, string> = {
|
||||
"Version (semver)": "版本(semver)",
|
||||
"Visibility": "可见性",
|
||||
"Needs you": "需要你处理",
|
||||
"Follow the clock": "跟随时间(夜间变暗)",
|
||||
"Next in line": "下一项工作",
|
||||
"Stopped: {reason}": "已停下:{reason}",
|
||||
"{n} more after these": "之后还有 {n} 项",
|
||||
|
||||
@@ -7,6 +7,7 @@ import "./styles/fonts.css";
|
||||
import "./styles.css";
|
||||
import "./styles/components.css";
|
||||
import "./overlays.css";
|
||||
import "./styles/pixel.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
width: min(520px, 100%);
|
||||
max-height: min(760px, 100%);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-xl);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 20px 60px rgba(24, 24, 24, 0.18);
|
||||
padding: var(--sp-5);
|
||||
animation: overlay-enter var(--motion-base) var(--ease-out-expo);
|
||||
}
|
||||
@@ -27,7 +23,7 @@
|
||||
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.modal-title { margin: 0 0 var(--sp-4); font-size: var(--fs-title); font-weight: 600; letter-spacing: -0.01em; }
|
||||
.modal-title { margin: 0 0 var(--sp-4); font-size: var(--fs-title); font-weight: 600; letter-spacing: 0; }
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-5); }
|
||||
|
||||
.onboarding-window {
|
||||
@@ -146,7 +142,7 @@
|
||||
padding: var(--sp-2) var(--sp-2) var(--sp-1);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.menu-row {
|
||||
|
||||
@@ -100,6 +100,7 @@ export function WorkspacePanel({
|
||||
<option value="system">{t("System")}</option>
|
||||
<option value="light">{t("Light")}</option>
|
||||
<option value="dark">{t("Dark")}</option>
|
||||
<option value="clock">{t("Follow the clock")}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<LanguageField uiPreferences={uiPreferences} actions={actions} />
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { UiTheme } from "../../../shared/snapshot-domain";
|
||||
|
||||
export type DayPart = "dawn" | "day" | "dusk" | "night";
|
||||
export type Season = "spring" | "summer" | "autumn" | "winter";
|
||||
|
||||
/** Local-hour bands the palette rotates through. */
|
||||
export function dayPart(now: Date): DayPart {
|
||||
const hour = now.getHours();
|
||||
if (hour >= 5 && hour < 8) return "dawn";
|
||||
if (hour >= 8 && hour < 17) return "day";
|
||||
if (hour >= 17 && hour < 20) return "dusk";
|
||||
return "night";
|
||||
}
|
||||
|
||||
/** Meteorological seasons by month, northern hemisphere. */
|
||||
export function season(now: Date): Season {
|
||||
const month = now.getMonth();
|
||||
if (month >= 2 && month <= 4) return "spring";
|
||||
if (month >= 5 && month <= 7) return "summer";
|
||||
if (month >= 8 && month <= 10) return "autumn";
|
||||
return "winter";
|
||||
}
|
||||
|
||||
export function resolveTheme(theme: UiTheme, prefersDark: boolean, now: Date): "light" | "dark" {
|
||||
if (theme === "system") return prefersDark ? "dark" : "light";
|
||||
if (theme === "clock") return dayPart(now) === "night" ? "dark" : "light";
|
||||
return theme;
|
||||
}
|
||||
|
||||
const TICK_MS = 60_000;
|
||||
|
||||
/** Applies the theme, day part and season to the document root, rechecked every minute and whenever the OS scheme changes. */
|
||||
export function usePaletteClock(theme: UiTheme): void {
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const [prefersDark, setPrefersDark] = useState(() => window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNow(new Date()), TICK_MS);
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onScheme = () => setPrefersDark(media.matches);
|
||||
media.addEventListener("change", onScheme);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
media.removeEventListener("change", onScheme);
|
||||
};
|
||||
}, []);
|
||||
const resolved = resolveTheme(theme, prefersDark, now);
|
||||
const part = dayPart(now);
|
||||
const time = season(now);
|
||||
useEffect(() => {
|
||||
Object.assign(document.documentElement.dataset, { theme: resolved, daypart: part, season: time });
|
||||
}, [resolved, part, time]);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/** CSS px per font pixel so each one lands on a whole number of screen pixels: the device ratio rounded to the nearest integer (at least 1), over the ratio. */
|
||||
export function pixelScale(devicePixelRatio: number): number {
|
||||
return Math.max(1, Math.round(devicePixelRatio)) / devicePixelRatio;
|
||||
}
|
||||
|
||||
/** Keeps --px on the root in step with the display, including when the window moves to another screen. */
|
||||
export function usePixelScale(): void {
|
||||
const [ratio, setRatio] = useState(() => window.devicePixelRatio);
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(`(resolution: ${ratio}dppx)`);
|
||||
const onChange = () => setRatio(window.devicePixelRatio);
|
||||
media.addEventListener("change", onChange);
|
||||
return () => media.removeEventListener("change", onChange);
|
||||
}, [ratio]);
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("--px", String(pixelScale(ratio)));
|
||||
}, [ratio]);
|
||||
}
|
||||
+90
-94
@@ -1,38 +1,34 @@
|
||||
/* Pixel design language: a Sweetie 16 based palette (GrafxKid), square corners, hard 2px shadows, and type at the pixel font's native 12px or 2x. */
|
||||
:root {
|
||||
/* Surfaces — pure-neutral gray ramp anchored on #181818 */
|
||||
--bg: #fafafa;
|
||||
--panel: #ffffff;
|
||||
--panel-2: #f6f6f6;
|
||||
--panel-3: #efefef;
|
||||
--hover: #f4f4f4;
|
||||
--border: #eaeaea;
|
||||
--border-strong: #e0e0e0;
|
||||
--border-soft: #f0f0f0;
|
||||
--bg: #dfe6ee;
|
||||
--panel: #f4f4f4;
|
||||
--panel-2: #e9eef3;
|
||||
--panel-3: #d5dde6;
|
||||
--hover: #e3e9ef;
|
||||
--border: #94b0c2;
|
||||
--border-strong: #333c57;
|
||||
--border-soft: #c9d5df;
|
||||
|
||||
/* Text — Primary #181818, Secondary #6A6A6A (+ derived muted/faint) */
|
||||
--text-1: #181818;
|
||||
--text-2: #6a6a6a;
|
||||
--text-3: #9a9a9a;
|
||||
--text-4: #c4c4c4;
|
||||
--text-1: #1a1c2c;
|
||||
--text-2: #333c57;
|
||||
--text-3: #566c86;
|
||||
--text-4: #94b0c2;
|
||||
|
||||
/* Accent + status */
|
||||
--accent: #5e6ad2;
|
||||
--accent-soft: #eef0ff;
|
||||
--green: #2f9e58;
|
||||
--green-soft: #e8f5ec;
|
||||
--amber: #c87d12;
|
||||
--amber-soft: #fbf3e2;
|
||||
--red: #d23b3b;
|
||||
--accent: #3b5dc9;
|
||||
--accent-soft: #c8d4f5;
|
||||
--green: #257a45;
|
||||
--green-soft: #d4f2dc;
|
||||
--amber: #a85a1d;
|
||||
--amber-soft: #fbe6c4;
|
||||
--red: #b13e53;
|
||||
|
||||
/* Radius — 8 → 40 scale */
|
||||
--r-sm: 8px;
|
||||
--r-md: 12px;
|
||||
--r-lg: 16px;
|
||||
--r-xl: 20px;
|
||||
--r-2xl: 28px;
|
||||
--r-full: 999px;
|
||||
--r-sm: 0;
|
||||
--r-md: 0;
|
||||
--r-lg: 0;
|
||||
--r-xl: 0;
|
||||
--r-2xl: 0;
|
||||
--r-full: 0;
|
||||
|
||||
/* Spacing — strict 4px grid */
|
||||
--sp-1: 4px;
|
||||
--sp-2: 8px;
|
||||
--sp-3: 12px;
|
||||
@@ -43,63 +39,62 @@
|
||||
--sp-8: 32px;
|
||||
--sp-10: 40px;
|
||||
|
||||
/* Type scale — Geist; body 14/16, title 18, display 24 */
|
||||
--fs-caption: 12px;
|
||||
--fs-body: 14px;
|
||||
--fs-body-lg: 16px;
|
||||
--fs-title: 18px;
|
||||
--fs-display: 24px;
|
||||
/* The pixel font is drawn on a 12px grid: text is 1x, headings 2x; --px (shared/pixel-scale.ts) keeps each font pixel on whole screen pixels. */
|
||||
--px: 1;
|
||||
--fs-caption: calc(12px * var(--px));
|
||||
--fs-body: calc(12px * var(--px));
|
||||
--fs-body-lg: calc(12px * var(--px));
|
||||
--fs-title: calc(12px * var(--px));
|
||||
--fs-display: calc(24px * var(--px));
|
||||
|
||||
/* Icons — 18px glyph, 1.5 stroke, bound in a 24px box */
|
||||
--icon-box: 24px;
|
||||
|
||||
--shadow-sm: 0 1px 0 rgba(24, 24, 24, 0.03), 0 1px 2px rgba(24, 24, 24, 0.05);
|
||||
--shadow-md: 0 1px 3px rgba(24, 24, 24, 0.06), 0 6px 24px rgba(24, 24, 24, 0.05);
|
||||
--font-sans: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
|
||||
--shadow-sm: 2px 2px 0 0 var(--border-soft);
|
||||
--shadow-md: 4px 4px 0 0 rgba(26, 28, 44, 0.35);
|
||||
--font-sans: "Fusion Pixel", monospace;
|
||||
--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||
--font-display: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif;
|
||||
--font-display: "Fusion Pixel", monospace;
|
||||
--motion-fast: 120ms;
|
||||
--motion-base: 200ms;
|
||||
--motion-slow: 280ms;
|
||||
--ease-out: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-out: steps(4, end);
|
||||
--ease-out-expo: steps(6, end);
|
||||
|
||||
/* Primary action — inverts against the theme */
|
||||
--btn-primary-bg: var(--text-1);
|
||||
--btn-primary-fg: #ffffff;
|
||||
--btn-primary-hover: #2c2c2c;
|
||||
--btn-primary-bg: var(--accent);
|
||||
--btn-primary-fg: #f4f4f4;
|
||||
--btn-primary-hover: #29366f;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #131313;
|
||||
--panel: #1b1b1b;
|
||||
--panel-2: #212121;
|
||||
--panel-3: #2a2a2a;
|
||||
--hover: #232323;
|
||||
--border: #2c2c2c;
|
||||
--border-strong: #3a3a3a;
|
||||
--border-soft: #242424;
|
||||
--bg: #1a1c2c;
|
||||
--panel: #262b44;
|
||||
--panel-2: #2c3352;
|
||||
--panel-3: #333c57;
|
||||
--hover: #2f3756;
|
||||
--border: #566c86;
|
||||
--border-strong: #94b0c2;
|
||||
--border-soft: #333c57;
|
||||
|
||||
--text-1: #ededed;
|
||||
--text-2: #a5a5a5;
|
||||
--text-3: #7c7c7c;
|
||||
--text-4: #4f4f4f;
|
||||
--text-1: #f4f4f4;
|
||||
--text-2: #c2d0dc;
|
||||
--text-3: #94b0c2;
|
||||
--text-4: #566c86;
|
||||
|
||||
--accent: #7c88e8;
|
||||
--accent-soft: #262a4a;
|
||||
--green: #45b06d;
|
||||
--green-soft: #16301f;
|
||||
--amber: #d99a3d;
|
||||
--amber-soft: #33270f;
|
||||
--red: #e05252;
|
||||
--accent: #41a6f6;
|
||||
--accent-soft: #29366f;
|
||||
--green: #a7f070;
|
||||
--green-soft: #1f3a2a;
|
||||
--amber: #ffcd75;
|
||||
--amber-soft: #3d3220;
|
||||
--red: #ef7d57;
|
||||
|
||||
--shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||
--shadow-md: 0 1px 3px rgba(0, 0, 0, 0.4), 0 6px 24px rgba(0, 0, 0, 0.35);
|
||||
--shadow-sm: 2px 2px 0 0 #10111c;
|
||||
--shadow-md: 4px 4px 0 0 rgba(0, 0, 0, 0.5);
|
||||
|
||||
--btn-primary-bg: #ededed;
|
||||
--btn-primary-fg: #131313;
|
||||
--btn-primary-hover: #ffffff;
|
||||
--btn-primary-bg: var(--accent);
|
||||
--btn-primary-fg: #1a1c2c;
|
||||
--btn-primary-hover: #73eff7;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@@ -107,14 +102,15 @@
|
||||
html, body, #root { width: 100%; height: 100%; margin: 0; }
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: "cv01", "ss03";
|
||||
color: var(--text-1);
|
||||
background: var(--bg);
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1.45;
|
||||
letter-spacing: -0.006em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
font-synthesis: none;
|
||||
-webkit-font-smoothing: none;
|
||||
text-rendering: optimizeSpeed;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.icon-box {
|
||||
width: var(--icon-box);
|
||||
@@ -176,14 +172,14 @@ a { color: inherit; }
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1a1a1d 0%, #3a3a40 100%);
|
||||
font-size: 11px;
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.brand-name {
|
||||
flex: 1;
|
||||
@@ -222,7 +218,7 @@ a { color: inherit; }
|
||||
color: var(--text-3);
|
||||
padding: var(--sp-3) var(--sp-2) var(--sp-2);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
@@ -279,7 +275,7 @@ a { color: inherit; }
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -328,7 +324,7 @@ a { color: inherit; }
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
padding: var(--sp-1) var(--sp-2);
|
||||
}
|
||||
.org-menu-row {
|
||||
@@ -353,7 +349,7 @@ a { color: inherit; }
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--motion-fast) var(--ease-out), background var(--motion-fast) var(--ease-out);
|
||||
@@ -428,8 +424,8 @@ a { color: inherit; }
|
||||
.brand-mark.sm {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 11px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--fs-body);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-spacer { flex: 1; }
|
||||
@@ -524,7 +520,7 @@ a { color: inherit; }
|
||||
bottom: -1px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border-radius: 0;
|
||||
border: 1.5px solid var(--bg);
|
||||
}
|
||||
.page { max-width: 1100px; }
|
||||
@@ -555,7 +551,7 @@ a { color: inherit; }
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--fs-title);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.014em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.page-sub {
|
||||
margin: 0 0 var(--sp-6);
|
||||
@@ -567,7 +563,7 @@ a { color: inherit; }
|
||||
color: var(--text-3);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.row { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
|
||||
.col { display: flex; flex-direction: column; min-width: 0; }
|
||||
@@ -606,10 +602,10 @@ a { color: inherit; }
|
||||
width: 26px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--text-2);
|
||||
font-size: 15px;
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1;
|
||||
transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out);
|
||||
}
|
||||
@@ -619,10 +615,10 @@ a { color: inherit; }
|
||||
min-width: 46px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
font-size: var(--fs-body);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.org-zoom-label:hover { background: var(--hover); color: var(--text-1); }
|
||||
@@ -650,7 +646,7 @@ a { color: inherit; }
|
||||
.org-node-name {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 560;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -675,4 +671,4 @@ a { color: inherit; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
|
||||
/* The object a link or search hit opened. */
|
||||
[data-revealed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
|
||||
[data-revealed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 0; }
|
||||
@@ -8,7 +8,7 @@
|
||||
color: var(--text-3);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -18,11 +18,11 @@
|
||||
gap: var(--sp-2);
|
||||
padding: 0 var(--sp-3);
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px solid var(--border-strong);
|
||||
border: 2px solid var(--border-strong);
|
||||
box-shadow: 0 2px 0 0 var(--border-strong);
|
||||
background: var(--panel);
|
||||
color: var(--text-1);
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background var(--motion-fast) var(--ease-out),
|
||||
@@ -32,10 +32,11 @@
|
||||
opacity var(--motion-fast) var(--ease-out);
|
||||
}
|
||||
.button:hover { background: var(--panel-2); }
|
||||
.button:active:not(:disabled) { transform: scale(0.97); }
|
||||
/* Pixel press: the 2px drop collapses. */
|
||||
.button:active:not(:disabled) { transform: translateY(2px); box-shadow: none; }
|
||||
.button.primary { background: var(--btn-primary-bg); color: var(--btn-primary-fg); border-color: var(--btn-primary-bg); }
|
||||
.button.primary:hover { background: var(--btn-primary-hover); }
|
||||
.button.ghost { border-color: transparent; background: transparent; color: var(--text-2); }
|
||||
.button.ghost { border-color: transparent; box-shadow: none; background: transparent; color: var(--text-2); }
|
||||
.button.ghost:hover { background: var(--hover); color: var(--text-1); }
|
||||
.button.small { height: 28px; padding: 0 var(--sp-2); font-size: var(--fs-caption); }
|
||||
.button:disabled { opacity: 0.45; cursor: default; }
|
||||
@@ -105,7 +106,7 @@
|
||||
transition: background var(--motion-base) var(--ease-out), color var(--motion-base) var(--ease-out), border-color var(--motion-base) var(--ease-out);
|
||||
}
|
||||
.pill .pill-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); flex-shrink: 0; transition: background var(--motion-base) var(--ease-out); }
|
||||
.pill .dot { width: 6px; height: 6px; border-radius: 0; background: var(--text-3); flex-shrink: 0; transition: background var(--motion-base) var(--ease-out); }
|
||||
.pill.amber { background: var(--amber-soft); color: var(--amber); border-color: transparent; }
|
||||
.pill.amber .dot { background: var(--amber); }
|
||||
.pill.green { background: var(--green-soft); color: var(--green); border-color: transparent; }
|
||||
@@ -121,21 +122,21 @@
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
border-radius: 0;
|
||||
color: #fff;
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.avatar.bot { border-radius: var(--r-sm); }
|
||||
.avatar.sm { width: 24px; height: 24px; font-size: 11px; }
|
||||
.avatar.sm { width: 24px; height: 24px; font-size: var(--fs-body); }
|
||||
.avatar.lg { width: 36px; height: 36px; font-size: var(--fs-body); }
|
||||
.avatar.xl { width: 48px; height: 48px; font-size: var(--fs-body-lg); }
|
||||
.avatar-stack { display: inline-flex; }
|
||||
.avatar-stack > .avatar-ring { margin-left: -6px; border: 2px solid var(--panel); border-radius: 50%; }
|
||||
.avatar-stack > .avatar-ring { margin-left: -6px; border: 2px solid var(--panel); border-radius: 0; }
|
||||
.avatar-stack > .avatar-ring:first-child { margin-left: 0; }
|
||||
|
||||
.num { font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
|
||||
.num { font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||||
.mono { font-family: var(--font-mono); }
|
||||
|
||||
.input {
|
||||
@@ -183,7 +184,7 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-weight: 540;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0;
|
||||
background: var(--panel-2);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
@@ -236,7 +237,7 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
.spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border-radius: 0;
|
||||
border: 2px solid var(--border-strong);
|
||||
border-top-color: var(--text-2);
|
||||
animation: spinner-rotate 0.7s linear infinite;
|
||||
@@ -248,7 +249,7 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
.avatar-editor { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.avatar-emoji-input { width: 72px; text-align: center; }
|
||||
.avatar-swatches { display: flex; gap: 6px; align-items: center; }
|
||||
.avatar-swatch { width: 18px; height: 18px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; }
|
||||
.avatar-swatch { width: 18px; height: 18px; border-radius: 0; border: 2px solid transparent; cursor: pointer; padding: 0; }
|
||||
.avatar-swatch.selected { border-color: var(--accent); }
|
||||
.avatar-swatch.auto { background: conic-gradient(#5e6ad2, #e8b339, #26b69a, #5e6ad2); }
|
||||
.sidebar-foot-button { display: flex; align-items: center; gap: 8px; width: 100%; background: none; border: none; padding: 0; cursor: pointer; color: inherit; font: inherit; text-align: left; border-radius: var(--r-sm); }
|
||||
@@ -266,10 +267,10 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
padding: 6px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 10px;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 8px 24px rgba(16, 16, 24, 0.18);
|
||||
}
|
||||
.office-legend { align-self: center; padding: 0 6px; font-size: 12px; color: var(--text-2); }
|
||||
.office-legend { align-self: center; padding: 0 6px; font-size: var(--fs-body); color: var(--text-2); }
|
||||
|
||||
/* Floating "jump to latest" pill above the chat composer */
|
||||
.jump-latest {
|
||||
@@ -281,12 +282,12 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--panel);
|
||||
color: var(--text-1);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 540;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(16, 16, 24, 0.18);
|
||||
@@ -321,10 +322,10 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
|
||||
.md-body p { margin: 0 0 6px; white-space: pre-wrap; }
|
||||
.md-body ul, .md-body ol { margin: 0 0 6px; padding-left: 22px; }
|
||||
.md-body li > ul, .md-body li > ol { margin: 2px 0; }
|
||||
.md-body h1, .md-body h2, .md-body h3 { font-size: 15px; font-weight: 600; margin: 8px 0 4px; }
|
||||
.md-body h1, .md-body h2, .md-body h3 { font-size: var(--fs-body); font-weight: 600; margin: 8px 0 4px; }
|
||||
.md-body blockquote { margin: 0 0 6px; padding-left: 10px; border-left: 3px solid var(--border-strong); color: var(--text-2); }
|
||||
.md-body code { font-family: var(--font-mono); background: var(--panel-3); padding: 1px 5px; border-radius: 4px; font-size: 0.92em; }
|
||||
.md-body pre { margin: 0 0 6px; padding: 10px 12px; background: var(--panel-3); border-radius: 8px; overflow-x: auto; }
|
||||
.md-body code { font-family: var(--font-mono); background: var(--panel-3); padding: 1px 5px; border-radius: 0; font-size: 0.92em; }
|
||||
.md-body pre { margin: 0 0 6px; padding: 10px 12px; background: var(--panel-3); border-radius: 0; overflow-x: auto; }
|
||||
.md-body pre code { background: transparent; padding: 0; white-space: pre; }
|
||||
.md-body table { border-collapse: collapse; margin: 0 0 6px; display: block; overflow-x: auto; max-width: 100%; }
|
||||
.md-body th, .md-body td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/* Geist is vendored locally (variable woff2) so the desktop build stays
|
||||
offline and CSP-clean (font-src 'self'). One file per family covers every
|
||||
weight via the variable axis. */
|
||||
/* Fonts are vendored so the build stays offline and CSP-clean (font-src 'self'). Fusion Pixel 12px (OFL, see fonts/fusion-pixel-licenses) covers Latin and Simplified Chinese. */
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
font-family: "Fusion Pixel";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url("../fonts/Geist-Variable.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../fonts/FusionPixel-12px-zh_hans.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Mono";
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Palette rotation: the day part tints the ground, the season sets the accent (shared/palette-clock.ts sets both on the root). */
|
||||
:root[data-theme="light"][data-daypart="dawn"] { --bg: #ebe1ea; --panel-2: #f1e9ef; }
|
||||
:root[data-theme="light"][data-daypart="dusk"] { --bg: #f0e0cf; --panel-2: #f6ebdf; }
|
||||
:root[data-theme="light"][data-daypart="night"] { --bg: #cfd7e4; --panel-2: #e1e7f0; }
|
||||
:root[data-theme="dark"][data-daypart="dawn"] { --bg: #221c30; --panel: #2c2540; }
|
||||
:root[data-theme="dark"][data-daypart="day"] { --bg: #1c2236; --panel: #27304a; }
|
||||
:root[data-theme="dark"][data-daypart="dusk"] { --bg: #2a1c2a; --panel: #34243a; }
|
||||
|
||||
:root[data-theme="light"][data-season="spring"] { --accent: #b03a6e; --accent-soft: #f6d3e3; --btn-primary-hover: #7a2450; }
|
||||
:root[data-theme="light"][data-season="autumn"] { --accent: #b4501d; --accent-soft: #f7d9c4; --btn-primary-hover: #7d3713; }
|
||||
:root[data-theme="light"][data-season="winter"] { --accent: #257179; --accent-soft: #c7e8ea; --btn-primary-hover: #174a50; }
|
||||
:root[data-theme="dark"][data-season="spring"] { --accent: #f49ac1; --accent-soft: #4a2238; --btn-primary-hover: #ffc4dd; }
|
||||
:root[data-theme="dark"][data-season="autumn"] { --accent: #ef7d57; --accent-soft: #4a2a1c; --btn-primary-hover: #ffcd75; }
|
||||
:root[data-theme="dark"][data-season="winter"] { --accent: #73eff7; --accent-soft: #1d4048; --btn-primary-hover: #c9fbff; }
|
||||
|
||||
/* 9-slice frame: an 8px pixel tile (outline, top-left light, bottom-right shade, fill) scaled 2x; corners are notched. */
|
||||
:root {
|
||||
--px-frame: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'%3E%3Cpath fill='%23333c57' d='M1 0h6v1H1zM1 7h6v1H1zM0 1h1v6H0zM7 1h1v6H7z'/%3E%3Cpath fill='%23ffffff' d='M1 1h6v1H1zM1 2h1v5H1z'/%3E%3Cpath fill='%2394b0c2' d='M2 6h5v1H2zM6 2h1v4H6z'/%3E%3Cpath fill='%23f4f4f4' d='M2 2h4v4H2z'/%3E%3C/svg%3E");
|
||||
--px-dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2' shape-rendering='crispEdges'%3E%3Cpath fill='%231a1c2c' fill-opacity='0.07' d='M0 0h1v1H0zM1 1h1v1H1z'/%3E%3C/svg%3E");
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--px-frame: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'%3E%3Cpath fill='%2394b0c2' d='M1 0h6v1H1zM1 7h6v1H1zM0 1h1v6H0zM7 1h1v6H7z'/%3E%3Cpath fill='%23566c86' d='M1 1h6v1H1zM1 2h1v5H1z'/%3E%3Cpath fill='%2310111c' d='M2 6h5v1H2zM6 2h1v4H6z'/%3E%3Cpath fill='%23262b44' d='M2 2h4v4H2z'/%3E%3C/svg%3E");
|
||||
--px-dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2' shape-rendering='crispEdges'%3E%3Cpath fill='%23f4f4f4' fill-opacity='0.05' d='M0 0h1v1H0zM1 1h1v1H1z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Ground: a faint 2x2 checker scaled to 4px cells, on every layer that paints the ground colour. */
|
||||
.app-shell, .sidebar, .main {
|
||||
background-image: var(--px-dither);
|
||||
background-size: 4px 4px;
|
||||
}
|
||||
|
||||
/* Dialogs are framed by the 9-slice tile at 2x. */
|
||||
.modal {
|
||||
border: 6px solid transparent;
|
||||
border-image: var(--px-frame) 3 fill / 6px stretch;
|
||||
background: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Menu arrow: the chosen or pointed-at nav row shows a blinking right-pointing pixel arrow. */
|
||||
.nav > .nav-item { position: relative; padding-left: 18px; }
|
||||
.nav > .nav-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 50%;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
margin-top: -5px;
|
||||
background: currentColor;
|
||||
clip-path: polygon(0 0, 34% 0, 34% 20%, 67% 20%, 67% 40%, 100% 40%, 100% 60%, 67% 60%, 67% 80%, 34% 80%, 34% 100%, 0 100%);
|
||||
visibility: hidden;
|
||||
}
|
||||
.nav > .nav-item:is(:hover, :focus-visible, [aria-current], .active)::before {
|
||||
visibility: visible;
|
||||
animation: px-blink 0.8s steps(1, end) infinite;
|
||||
}
|
||||
@keyframes px-blink { 50% { visibility: hidden; } }
|
||||
@@ -54,7 +54,8 @@ import type {
|
||||
SocialAuditRecord,
|
||||
} from "./social-domain";
|
||||
|
||||
export type UiTheme = "system" | "light" | "dark";
|
||||
/** "clock" turns dark at night and light by day. */
|
||||
export type UiTheme = "system" | "light" | "dark" | "clock";
|
||||
export type UiLanguage = "en" | "zh";
|
||||
|
||||
export interface UiPreferences {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { _electron as electron, expect, test } from "@playwright/test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture";
|
||||
import { closeApp, onboard } from "./onboard";
|
||||
|
||||
test("text uses the bundled pixel font, and the palette follows the day part, season and clock theme", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-e2e-pixel-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-pixel-builtin-${randomUUID()}`));
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir },
|
||||
});
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await onboard(page, "Pixel Co");
|
||||
// The font file itself loaded, not a fallback.
|
||||
expect(await page.evaluate(async () => {
|
||||
await document.fonts.load('12px "Fusion Pixel"', "像素 Pixel");
|
||||
return [...document.fonts].some((face) => face.family.replace(/"/g, "") === "Fusion Pixel" && face.status === "loaded");
|
||||
})).toBe(true);
|
||||
expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toMatch(/^"?Fusion Pixel/);
|
||||
|
||||
const expected = await page.evaluate(() => {
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
const month = now.getMonth();
|
||||
const part = hour >= 5 && hour < 8 ? "dawn" : hour >= 8 && hour < 17 ? "day" : hour >= 17 && hour < 20 ? "dusk" : "night";
|
||||
const season = month >= 2 && month <= 4 ? "spring" : month >= 5 && month <= 7 ? "summer" : month >= 8 && month <= 10 ? "autumn" : "winter";
|
||||
return { part, season };
|
||||
});
|
||||
const root = page.locator("html");
|
||||
await expect(root).toHaveAttribute("data-daypart", expected.part);
|
||||
await expect(root).toHaveAttribute("data-season", expected.season);
|
||||
|
||||
// The ground carries the dither; the chosen nav row blinks the pixel menu arrow, other rows show none; dialogs sit in the 9-slice frame.
|
||||
expect(await page.evaluate(() => getComputedStyle(document.querySelector(".main")!).backgroundImage)).toContain("data:image/svg+xml");
|
||||
expect(await page.evaluate(() => {
|
||||
const rows = [...document.querySelectorAll(".nav > .nav-item")];
|
||||
const active = rows.find((row) => row.matches(".active, [aria-current]"))!;
|
||||
const idle = rows.find((row) => !row.matches(".active, [aria-current]"))!;
|
||||
return [getComputedStyle(active, "::before").animationName, getComputedStyle(idle, "::before").animationName, getComputedStyle(idle, "::before").visibility];
|
||||
})).toEqual(["px-blink", "none", "hidden"]);
|
||||
await page.getByRole("button", { name: "New task" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "New task" });
|
||||
await expect(dialog).toBeVisible();
|
||||
expect(await dialog.evaluate((node) => {
|
||||
const style = getComputedStyle(node.closest(".modal") ?? node.querySelector(".modal")!);
|
||||
return [style.borderImageSource.startsWith('url("data:image/svg+xml'), style.borderImageSlice, style.borderTopWidth];
|
||||
})).toEqual([true, "3 fill", "6px"]);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
await page.getByRole("combobox", { name: "Theme" }).selectOption("dark");
|
||||
await expect(root).toHaveAttribute("data-theme", "dark");
|
||||
await page.getByRole("combobox", { name: "Theme" }).selectOption({ label: "Follow the clock" });
|
||||
await expect(root).toHaveAttribute("data-theme", expected.part === "night" ? "dark" : "light");
|
||||
|
||||
// Each font pixel covers a whole number of screen pixels at this display's scale.
|
||||
expect(await page.evaluate(() => {
|
||||
const fontPx = parseFloat(getComputedStyle(document.body).fontSize) * window.devicePixelRatio;
|
||||
return Math.abs(fontPx - Math.round(fontPx)) < 1e-6 && Math.round(fontPx) % 12 === 0;
|
||||
})).toBe(true);
|
||||
|
||||
} finally {
|
||||
await closeApp(app);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { dayPart, resolveTheme, season } from "../../src/renderer/src/shared/palette-clock";
|
||||
|
||||
const at = (month: number, hour: number, minute = 0) => new Date(2026, month - 1, 15, hour, minute);
|
||||
|
||||
test("the day turns dawn, day, dusk and night at fixed local hours", () => {
|
||||
expect(dayPart(at(6, 4, 59))).toBe("night");
|
||||
expect(dayPart(at(6, 5))).toBe("dawn");
|
||||
expect(dayPart(at(6, 7, 59))).toBe("dawn");
|
||||
expect(dayPart(at(6, 8))).toBe("day");
|
||||
expect(dayPart(at(6, 16, 59))).toBe("day");
|
||||
expect(dayPart(at(6, 17))).toBe("dusk");
|
||||
expect(dayPart(at(6, 19, 59))).toBe("dusk");
|
||||
expect(dayPart(at(6, 20))).toBe("night");
|
||||
expect(dayPart(at(6, 0))).toBe("night");
|
||||
});
|
||||
|
||||
test("seasons follow the months", () => {
|
||||
expect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((month) => season(at(month, 12)))).toEqual([
|
||||
"winter", "winter", "spring", "spring", "spring", "summer", "summer", "summer", "autumn", "autumn", "autumn", "winter",
|
||||
]);
|
||||
});
|
||||
|
||||
test("light and dark are kept as chosen; system follows the OS; the clock goes dark only at night", () => {
|
||||
expect(resolveTheme("light", true, at(1, 23))).toBe("light");
|
||||
expect(resolveTheme("dark", false, at(1, 12))).toBe("dark");
|
||||
expect(resolveTheme("system", true, at(1, 12))).toBe("dark");
|
||||
expect(resolveTheme("system", false, at(1, 23))).toBe("light");
|
||||
expect(resolveTheme("clock", true, at(1, 12))).toBe("light");
|
||||
expect(resolveTheme("clock", false, at(1, 18))).toBe("light");
|
||||
expect(resolveTheme("clock", false, at(1, 21))).toBe("dark");
|
||||
expect(resolveTheme("clock", true, at(1, 4))).toBe("dark");
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { pixelScale } from "../../src/renderer/src/shared/pixel-scale";
|
||||
|
||||
test("one font pixel always covers a whole number of screen pixels, never fewer than one", () => {
|
||||
expect(pixelScale(1)).toBe(1);
|
||||
expect(pixelScale(2)).toBe(1);
|
||||
expect(pixelScale(1.5)).toBeCloseTo(4 / 3);
|
||||
expect(pixelScale(1.25)).toBeCloseTo(0.8);
|
||||
expect(pixelScale(1.75)).toBeCloseTo(8 / 7);
|
||||
expect(pixelScale(0.9)).toBeCloseTo(1 / 0.9);
|
||||
for (const ratio of [1, 1.25, 1.5, 1.75, 2, 2.5, 3]) {
|
||||
const device = 12 * pixelScale(ratio) * ratio;
|
||||
expect(Math.abs(device - Math.round(device))).toBeLessThan(1e-9);
|
||||
expect(Math.round(device) % 12).toBe(0);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user