diff --git a/assets/RemixIcon-License.txt b/assets/RemixIcon-License.txt new file mode 100644 index 0000000..996917e --- /dev/null +++ b/assets/RemixIcon-License.txt @@ -0,0 +1,186 @@ +Remix Icon License v1.0 +Version 1.0 – January 2026 +Copyright (c) 2017–2026 Remix Design + + This License governs the use of the Remix Icon library, including all icons, + SVG files, font files, and related assets (collectively, the "Icons"). + +1. Grant of License + + Subject to the terms and conditions of this License, Remix Design grants you + a worldwide, royalty-free, non-exclusive license to use, copy, modify, + merge, and distribute the Icons, as permitted below. + +2. Permitted Uses + + You are permitted to: + + 2.1 Commercial and Non-Commercial Use + Use the Icons in personal, commercial, and client projects, including + but not limited to: + - Websites, web applications, and mobile applications + - Software products and SaaS platforms + - User interface designs, design systems, and UI kits + - Presentations, documentation, and marketing materials + - Physical or digital products where the Icons are decorative or + functional elements + + 2.2 Modification and Derivative Works + - Modify, adapt, or customize the Icons + - Change colors, sizes, stroke styles, or other visual attributes + to fit your project needs + + 2.3 Distribution as Part of a Larger Work + Include the Icons (modified or unmodified) as part of a larger product, + provided that: + - The Icons are functional or decorative components of the product, and + - The Icons are not marketed or sold as the primary value of the product + + 2.4 Attribution (Optional) + Attribution to Remix Icon is appreciated but not required. + Suggested attribution: + "Icons by Remix Icon (https://remixicon.com)" + +3. Restrictions + + 3.1 Prohibited: Standalone Icon Sales + You may NOT: + - Sell, sublicense, or distribute the Icons as a standalone product + - Package the Icons as an independent icon pack, icon library, or + icon font for sale + - Offer the Icons as a downloadable asset collection where the Icons + themselves constitute the primary product value + + Clarification: + A product is considered a prohibited standalone icon sale if: + - The Icons are the primary advertised feature, or + - Removing the Icons would eliminate the core value of the product + + Examples: + - Prohibited: "Premium Icon Pack – 2,000+ Icons for $29" + - Prohibited: "Extended Remix Icon Library (Paid)" + - Permitted: Website or app templates that include Icons as UI elements + - Permitted: Design systems or UI kits where Icons are a minor component + + 3.2 Prohibited: Competing Icon Libraries + You may NOT use the Icons to create, distribute, or sell a competing + icon library or icon set, even if the Icons are modified with only + minor or superficial changes (such as color, size, stroke width, + corner radius, or layout). + + 3.3 Prohibited: Logo, Trademark, and Brand Identity Use + You may NOT use the Icons, whether modified or unmodified, as: + - A logo, trademark, or brand identifier + - The primary visual identity of a company, product, or service + + This includes, but is not limited to: + - Creating a logo by applying simple modifications such as color changes, + rotation, combination, or layout + - Using an Icon as a brand mark, app icon, or identity symbol where the + Icon itself represents the brand + + Rationale: + Remix Icon is designed for functional and informational use. Using the + Icons as brand identifiers may impair their recognizability and neutrality + in other functional contexts. + +4. Brand Icons and Trademarks + + Some Icons represent third-party brands or services. + + 4.1 Permitted Use of Brand Icons + You may use brand icons solely to represent or link to the corresponding + brand, product, or service. + Examples: + - Using a GitHub icon to link to a GitHub repository + - Displaying social media icons in a website footer + + 4.2 Prohibited Use of Brand Icons + You may NOT: + - Use brand icons as your own logo or trademark + - Modify brand icons to create a new brand identity + - Use brand icons in a way that implies endorsement or affiliation + by the brand owner + - Use brand icons for purposes unrelated to the represented brand + + 4.3 Trademark Responsibility + All brand icons are subject to the trademark rights of their respective + owners. This License does not grant any trademark rights. + Users are responsible for ensuring that their use of brand icons complies + with applicable trademark laws. + +5. Copyright and License Notice + + When distributing the complete Icon library or substantial portions thereof: + + - You must retain existing copyright notices in source files + - You should include a copy of this License or a link to it + + For individual use of Icons within projects (e.g., compiled apps, websites, + presentations), no copyright notice is required. + +6. No Trademark Rights + + This License does not grant permission to use: + + - The "Remix Icon" name, logo, or trademarks + - Any trademarks represented by brand icons + + You may refer to Remix Icon by name solely for attribution purposes. + +7. Disclaimer of Warranty + + THE ICONS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. + +8. Limitation of Liability + + IN NO EVENT SHALL REMIX DESIGN BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING + FROM, OUT OF, OR IN CONNECTION WITH THE ICONS OR THEIR USE. + +9. License Compatibility + + You may integrate the Icons into projects under permissive licenses + (such as MIT, BSD, or Apache 2.0), provided that: + + - The Icons themselves remain governed by this License + - The restrictions in Section 3 continue to apply + + This License may be incompatible with licenses that require unrestricted + redistribution of assets as standalone products or mandatory application + of strong copyleft or share-alike terms. + +10. Termination + + Your rights under this License terminate automatically if you breach its + terms. + + Upon termination: + + - You must cease any distribution that violates this License + - You may continue to use the Icons in projects already distributed + prior to termination + - End users who received such projects are not affected + +11. License Versioning + + Remix Design may publish revised versions of this License. + + You may choose to use the Icons under: + + - The version of this License under which you received them, or + - Any later version published by Remix Design + + Once chosen, that version may be used indefinitely. + +How to Apply This License + + Include the following notice in your documentation when appropriate: + + "This project uses icons from Remix Icon (https://remixicon.com), + licensed under the Remix Icon License v1.0." + + When distributing the complete Icon library or substantial portions of it, + include a copy of this License. diff --git a/assets/readme-demo-zh-CN.png b/assets/readme-demo-zh-CN.png index 7fd19f6..809de9b 100644 Binary files a/assets/readme-demo-zh-CN.png and b/assets/readme-demo-zh-CN.png differ diff --git a/assets/readme-demo.png b/assets/readme-demo.png index e314162..36e7524 100644 Binary files a/assets/readme-demo.png and b/assets/readme-demo.png differ diff --git a/assets/remix-earth-line.svg b/assets/remix-earth-line.svg new file mode 100644 index 0000000..7ba7f49 --- /dev/null +++ b/assets/remix-earth-line.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/design.md b/docs/design.md index 5decf61..53fccfd 100644 --- a/docs/design.md +++ b/docs/design.md @@ -21,11 +21,14 @@ wide desktop with enough card width, then collapse to two and one card without i Focused dialogs may handle add-account, explicit confirmation, progress/results, or recovery; they do not create a second navigation system. -The toolbar contains the current Codex account, Refresh, Wake all, Add account, -and a compact language menu. It is a command bar, not a dashboard header -or a custom window chrome. Its status dot always remains visible; only a long -account label may truncate. The status is also written in text so current, unknown, -signed-out, setup, and recovery states do not rely on color. +The toolbar contains the vertically centered GSwitch mark and name, Refresh, +Wake all, Add account, and a compact language menu. The current account is +identified on its card rather than repeated beside the brand. The language +button uses Remix Icon's `earth-line` at 18px, inheriting the current theme +color. It is a command bar, not a dashboard header or custom window chrome. +Unknown, setup, and recovery states use the existing actionable notice. With +saved accounts, a signed-out Codex uses one quiet text notice without another +button; without saved accounts, the empty state provides the next step. An account card should show only the state needed for a decision: @@ -90,7 +93,7 @@ points to the official install guide. A failed check remains quiet; no update is installed without a click. - Do not optimistically display a switch or redemption as complete. -- After a verified switch, update the toolbar identity and active card directly. +- After a verified switch, update the active card directly. That state is the visible confirmation; do not insert a success banner above the account grid or reload the whole workspace before showing it. Other brief success and information messages use one compact floating notice without @@ -147,8 +150,8 @@ is installed without a click. **Sign in again** its primary action. The focused dialog shows the saved email and workspace, offers Copy email, and updates the original record only after verifying the returning identity. A saved login for the current account - that could not be applied shows **Apply**. The toolbar identifies the selected - local Codex account without claiming that its remote sign-in is valid. + that could not be applied shows **Apply**. The current badge identifies the + selected local Codex account without claiming that its remote sign-in is valid. - After Wake, sign-in, or refresh, update affected cards in place. Preserve order and last known quota while a new provider projection is pending; old results cannot replace a newer login or clear the entire account grid. diff --git a/docs/testing.md b/docs/testing.md index e7f5f75..3f5a198 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -247,8 +247,9 @@ as applicable: and aggregate-only command output; - Add Account import-first hierarchy, account selection controls, selected Wake, unencrypted-export confirmation, and result feedback without credential data. -- the toolbar's visible ready indicator beside a separately truncatable account - label, plus the canonical SVG and generated native/installer icon dimensions. +- the toolbar's centered brand without a duplicate current-account label, + current-card identification, signed-out and recovery notices, theme-aware + language icon, plus the canonical SVG and generated native/installer icon dimensions. - system-language selection, unsupported-locale fallback, immediate manual language selection, language preference persistence, locale-aware reset-time formatting, and the main account flow in Simplified Chinese. diff --git a/src/App.test.tsx b/src/App.test.tsx index 8eb97de..cc77873 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -392,18 +392,36 @@ describe("GSwitch account workspace", () => { await waitFor(() => expect(dialog).not.toBeInTheDocument()); }); - it("keeps the current account label visible without claiming sign-in readiness", async () => { + it("shows the current account on its card without repeating it in the toolbar", async () => { + const account = { ...chatAccount, email: "a-very-long-account-name-that-must-truncate@example.com", active: true }; + mocks.listAccounts.mockResolvedValue([account]); mocks.liveAccount.mockResolvedValue({ status: "ready", credential_store: "file", - account: { label: "a-very-long-account-name-that-must-truncate@example.com" }, + account, }); const { container } = render(); + const card = (await screen.findByRole("heading", { name: account.email })).closest("article")!; + expect(within(card).getByText("Current", { selector: ".badge-active" })).toBeInTheDocument(); + expect(container.querySelector("header")).not.toHaveTextContent(account.email); + expect(within(container.querySelector("header")!).getByRole("heading", { name: "GSwitch" })).toBeInTheDocument(); + }); + + it("shows a signed-out notice with saved accounts and no extra action", async () => { + mocks.listAccounts.mockResolvedValue([chatAccount]); + render(); + const notice = await screen.findByText("Codex is signed out"); + expect(notice).toHaveAttribute("role", "status"); + expect(notice.querySelector("button")).toBeNull(); + expect(screen.getByRole("button", { name: "Switch to person@example.com" })).toBeInTheDocument(); + }); + + it("uses the empty state without a duplicate signed-out notice", async () => { + render(); await screen.findByRole("heading", { name: "0 saved accounts" }); - expect(container.querySelector(".brand-status > .status-dot")).toBeInTheDocument(); - expect(container.querySelector(".brand-status > .status-ready")).not.toBeInTheDocument(); - expect(container.querySelector(".brand-status-label")).toHaveTextContent("a-very-long-account-name-that-must-truncate@example.com"); + expect(screen.getByRole("heading", { name: "Add your Codex accounts" })).toBeInTheDocument(); + expect(screen.queryByText("Codex is signed out")).not.toBeInTheDocument(); }); it("does not scan local account sources at startup or when the method row opens", async () => { diff --git a/src/App.tsx b/src/App.tsx index 4da7e3b..4e4332f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2018,14 +2018,6 @@ export default function App() { const cliUpdateAvailable = cliInfo?.update_status === "available" && cliInfo.supports_update; const currentActiveId = live?.account?.id; refreshInputs.current = { accounts, quotas, quotaFailures, activeId: currentActiveId }; - const liveAccountLabel = live?.account ? accountPrimaryName(live.account) : t("toolbar.currentAccount"); - const liveStatusLabel = !live ? t("toolbar.statusChecking") : t(({ - ready: "toolbar.statusReady", - not_signed_in: "toolbar.statusSignedOut", - unknown_account: "toolbar.statusUnknown", - file_store_required: "toolbar.statusFileStore", - recovery_required: "toolbar.statusRecovery", - } as const)[live.status]); const chatGptAccounts = accounts.filter((account) => account.kind === "chat_gpt"); const selectedChatGptCount = accounts.filter( (account) => account.kind === "chat_gpt" && selectedAccountIds.includes(account.id), @@ -2113,15 +2105,7 @@ export default function App() {
-
-

GSwitch

-

-

-
+

GSwitch