From e525f3fb00755f5d252606e57b408f29541c1a0b Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Jun 2026 16:24:48 +0100 Subject: [PATCH 1/3] chore(dotcom): clean up dialogs and remove dead code (#9261) This PR reduces dead code in the dotcom client and tidies up several dialogs. Most of the diff is dead-code removal with no behavior change; the dialog work is a small set of deliberate sizing, layout, and copy tweaks. ### Dead code removal Every removed rule, component, export, and the `warning` button variant was verified to have no remaining references in the codebase. - Unused CSS across admin, sidebar, dialogs, auth, menu, editor, share zone, document name, and globals stylesheets. - The `warning` variant from `TlaButton` and its consumers (`admin.tsx`, `file-share-menu-primitives.tsx`). - The orphaned `ExternalLink.module.css` and `icon.module.css` wrapper rules. - Unused components/exports: `LegacyLinks`, `TlaIconWrapper`, `ColorThemeSubmenu`, `DotDevMenuItem`, `TldrawAppUiEventsProvider`, and `shouldClearDocument`, plus their now-unused messages. ### Dialog tweaks - Prevent the invite sign-in dialog from being dismissed by clicking the background overlay. - Use 36px inputs and buttons in the auth dialog (was 40px). - Add a `big` (36px) variant to `TlaButton` and normalize the workspace settings dialog sizing, label rows, and invite-link buttons. - Tighten invite and sign-in copy, and adjust the create-workspace dialog field layout. - Simplify the menu switch hover styles. ### Change type - [x] `other` ### Test plan Mostly dead-code removal with no behavior change; verified by confirming no remaining references to any removed CSS class, component, export, or the `warning` variant. Smoke-test the dotcom client to confirm no visual regressions in the affected areas (admin, sidebar, dialogs, auth, menus, editor), that the invite sign-in dialog cannot be dismissed by an overlay click, and that auth/settings inputs and buttons render at the new 36px sizing. ### Code changes | Section | LOC change | | ------- | ---------- | | Apps | +59 / -878 | --- .../e2e/fixtures/WorkspaceInviteDialog.ts | 2 +- .../public/tla/locales-compiled/en.json | 60 +++-- apps/dotcom/client/public/tla/locales/en.json | 18 +- apps/dotcom/client/src/components/Links.tsx | 46 ---- apps/dotcom/client/src/pages/admin.module.css | 43 ---- apps/dotcom/client/src/pages/admin.tsx | 5 +- .../ExternalLink/ExternalLink.module.css | 6 - .../tla/components/TlaButton/TlaButton.tsx | 5 +- .../components/TlaButton/button.module.css | 15 +- .../components/TlaEditor/editor.module.css | 32 --- .../tla/components/TlaEditor/top.module.css | 4 - .../file-share-menu-primitives.tsx | 2 +- .../src/tla/components/TlaIcon/TlaIcon.tsx | 6 +- .../tla/components/TlaIcon/icon.module.css | 24 -- .../components/TlaSidebar/sidebar.module.css | 151 ----------- .../tla/components/WorkspaceInviteHandler.tsx | 1 + .../dialogs/CreateWorkspaceDialog.tsx | 14 +- .../components/dialogs/TlaInviteDialog.tsx | 2 +- .../components/dialogs/TlaSignInDialog.tsx | 9 +- .../tla/components/dialogs/auth.module.css | 71 +---- .../tla/components/dialogs/dialogs.module.css | 50 +--- .../tla/components/menu-items/menu-items.tsx | 26 -- .../tla/components/tla-menu/menu.module.css | 26 +- apps/dotcom/client/src/tla/styles/tla.css | 45 ---- .../client/src/tla/utils/app-ui-events.tsx | 17 +- .../client/src/utils/shouldClearDocument.tsx | 58 ----- apps/dotcom/client/styles/globals.css | 243 ------------------ 27 files changed, 85 insertions(+), 896 deletions(-) delete mode 100644 apps/dotcom/client/src/components/Links.tsx delete mode 100644 apps/dotcom/client/src/tla/components/ExternalLink/ExternalLink.module.css delete mode 100644 apps/dotcom/client/src/utils/shouldClearDocument.tsx diff --git a/apps/dotcom/client/e2e/fixtures/WorkspaceInviteDialog.ts b/apps/dotcom/client/e2e/fixtures/WorkspaceInviteDialog.ts index 926bf10484db..7f925bafaa56 100644 --- a/apps/dotcom/client/e2e/fixtures/WorkspaceInviteDialog.ts +++ b/apps/dotcom/client/e2e/fixtures/WorkspaceInviteDialog.ts @@ -7,7 +7,7 @@ export class WorkspaceInviteDialog { public readonly declineButton: Locator constructor(public readonly page: Page) { - this.dialog = this.page.getByText('You have been invited to join workspace') + this.dialog = this.page.getByText('You have been invited to join:') this.acceptButton = this.page.getByRole('button', { name: 'Accept and join workspace' }) this.declineButton = this.page.getByRole('button', { name: 'No thanks' }) } diff --git a/apps/dotcom/client/public/tla/locales-compiled/en.json b/apps/dotcom/client/public/tla/locales-compiled/en.json index e5a607953839..e2c07241f232 100644 --- a/apps/dotcom/client/public/tla/locales-compiled/en.json +++ b/apps/dotcom/client/public/tla/locales-compiled/en.json @@ -111,12 +111,6 @@ "value": "Unpin file" } ], - "12c8a9838c": [ - { - "type": 0, - "value": "Try the tldraw SDK" - } - ], "13ddd375ad": [ { "type": 0, @@ -159,6 +153,26 @@ "value": "Workspace name" } ], + "23dcace54a": [ + { + "type": 0, + "value": "You have been invited to join " + }, + { + "children": [ + { + "type": 1, + "value": "workspaceName" + } + ], + "type": 8, + "value": "strong" + }, + { + "type": 0, + "value": ". Create a free account to continue." + } + ], "250ddea65c": [ { "type": 0, @@ -385,6 +399,12 @@ "value": "PNG" } ], + "5638176be5": [ + { + "type": 0, + "value": "Create a workspace" + } + ], "5677e381d2": [ { "type": 0, @@ -619,6 +639,12 @@ "value": "User manual" } ], + "7bcdfeab70": [ + { + "type": 0, + "value": "You have been invited to join:" + } + ], "7be10fc592": [ { "type": 0, @@ -927,22 +953,6 @@ "value": "File is getting large" } ], - "a8c9ad7ea5": [ - { - "type": 0, - "value": "You have been invited to join " - }, - { - "children": [ - { - "type": 1, - "value": "workspaceName" - } - ], - "type": 8, - "value": "strong" - } - ], "a9ded1e5ce": [ { "type": 0, @@ -1321,12 +1331,6 @@ "value": "or" } ], - "e9e0f16850": [ - { - "type": 0, - "value": "You have been invited to join workspace:" - } - ], "ea4788705e": [ { "type": 0, diff --git a/apps/dotcom/client/public/tla/locales/en.json b/apps/dotcom/client/public/tla/locales/en.json index db431d5cf607..34174d92e9ee 100644 --- a/apps/dotcom/client/public/tla/locales/en.json +++ b/apps/dotcom/client/public/tla/locales/en.json @@ -53,9 +53,6 @@ "12a6766ba1": { "translation": "Unpin file" }, - "12c8a9838c": { - "translation": "Try the tldraw SDK" - }, "13ddd375ad": { "translation": "Export as" }, @@ -77,6 +74,9 @@ "21f66c9c00": { "translation": "Workspace name" }, + "23dcace54a": { + "translation": "You have been invited to join {workspaceName}. Create a free account to continue." + }, "250ddea65c": { "translation": "About tldraw" }, @@ -185,6 +185,9 @@ "55505ba281": { "translation": "PNG" }, + "5638176be5": { + "translation": "Create a workspace" + }, "5677e381d2": { "translation": "We use analytics cookies to make tldraw better." }, @@ -302,6 +305,9 @@ "7b1329f5ca": { "translation": "User manual" }, + "7bcdfeab70": { + "translation": "You have been invited to join:" + }, "7be10fc592": { "translation": "Restore version" }, @@ -431,9 +437,6 @@ "a8873770ab": { "translation": "File is getting large" }, - "a8c9ad7ea5": { - "translation": "You have been invited to join {workspaceName}" - }, "a9ded1e5ce": { "translation": "Background" }, @@ -576,9 +579,6 @@ "e81c4e4f2b": { "translation": "or" }, - "e9e0f16850": { - "translation": "You have been invited to join workspace:" - }, "ea4788705e": { "translation": "Cancel" }, diff --git a/apps/dotcom/client/src/components/Links.tsx b/apps/dotcom/client/src/components/Links.tsx deleted file mode 100644 index 51ac78549d5b..000000000000 --- a/apps/dotcom/client/src/components/Links.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { TldrawUiMenuGroup, TldrawUiMenuItem } from 'tldraw' -import { useOpenUrlAndTrack } from '../hooks/useOpenUrlAndTrack' - -export function LegacyLinks() { - const openAndTrack = useOpenUrlAndTrack('main-menu') - - return ( - <> - - { - openAndTrack( - 'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/TERMS_OF_SERVICE.md' - ) - }} - /> - { - openAndTrack( - 'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/PRIVACY_POLICY.md' - ) - }} - /> - - - { - openAndTrack( - 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=learn-more', - true - ) - }} - /> - - - ) -} diff --git a/apps/dotcom/client/src/pages/admin.module.css b/apps/dotcom/client/src/pages/admin.module.css index 3efef117cad3..af1f6a2279c1 100644 --- a/apps/dotcom/client/src/pages/admin.module.css +++ b/apps/dotcom/client/src/pages/admin.module.css @@ -318,49 +318,6 @@ flex-wrap: wrap; } -/* Invites Table */ -.invitesTable { - width: 100%; - border-collapse: collapse; - background-color: var(--tla-color-panel); - border: 1px solid var(--tla-color-border); - border-radius: var(--tla-radius-2); - overflow: hidden; -} - -.invitesTable th { - text-align: left; - padding: 12px; - background-color: var(--tla-color-low); - border-bottom: 1px solid var(--tla-color-border); - font-size: 11px; - font-weight: 600; - color: var(--tla-color-text-3); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.invitesTable td { - text-align: left; - padding: 12px; - border-bottom: 1px solid var(--tla-color-border); - font-size: 12px; - color: var(--tla-color-text-1); -} - -.invitesTable tbody tr:last-child td { - border-bottom: none; -} - -.invitesTable tbody tr:hover { - background-color: var(--tla-color-low); -} - -.tableActions { - display: flex; - gap: 8px; -} - /* Feature Flags */ .featureFlagsNote { margin-bottom: 8px; diff --git a/apps/dotcom/client/src/pages/admin.tsx b/apps/dotcom/client/src/pages/admin.tsx index 7b0fe6e367dc..4e681fb8ac34 100644 --- a/apps/dotcom/client/src/pages/admin.tsx +++ b/apps/dotcom/client/src/pages/admin.tsx @@ -217,7 +217,6 @@ export function Component() { @@ -685,7 +684,7 @@ function HardDeleteFile() { {successMessage &&
{successMessage}
}
- + Delete (cannot be undone)
@@ -1206,7 +1205,6 @@ function RolloutWorkspacesUi() {
(function TlaButton( { @@ -23,6 +24,7 @@ export const TlaButton = forwardRef< ghost = false, variant = 'primary', isLoading = false, + big = false, onClick, ...props }, @@ -42,6 +44,7 @@ export const TlaButton = forwardRef< [styles.primary]: variant === 'primary', [styles.secondary]: variant === 'secondary', [styles.ghost]: ghost, + [styles.big]: big, }, className )} diff --git a/apps/dotcom/client/src/tla/components/TlaButton/button.module.css b/apps/dotcom/client/src/tla/components/TlaButton/button.module.css index ede4a53014db..907f99158d42 100644 --- a/apps/dotcom/client/src/tla/components/TlaButton/button.module.css +++ b/apps/dotcom/client/src/tla/components/TlaButton/button.module.css @@ -47,6 +47,11 @@ animation: tla-spin 1.5s linear infinite; } +.big { + height: 36px; + border-radius: var(--tl-radius-2); +} + @keyframes tla-spin { 0% { transform: rotate(0deg); @@ -82,11 +87,6 @@ color: var(--tla-color-text-1); } -.warning { - background-color: var(--tla-color-secondary); - color: var(--tla-color-warning); -} - .iconRight { position: absolute; right: 8px; @@ -122,11 +122,6 @@ border: none; } -.warning.ghost { - color: var(--tla-color-warning); - background-color: transparent; -} - @media (hover: hover) { .primary:not(:disabled):hover { background-color: var(--tla-color-primary-hover); diff --git a/apps/dotcom/client/src/tla/components/TlaEditor/editor.module.css b/apps/dotcom/client/src/tla/components/TlaEditor/editor.module.css index 51a3ed7e9c09..8c25c8d39be7 100644 --- a/apps/dotcom/client/src/tla/components/TlaEditor/editor.module.css +++ b/apps/dotcom/client/src/tla/components/TlaEditor/editor.module.css @@ -6,35 +6,3 @@ z-index: 1; background-color: var(--tl-color-background); } - -.editorWrapper { - position: relative; - flex-grow: 2; - overflow: hidden; - border-top: 1px solid var(--tla-color-border); - background-color: var(--tl-color-background); -} - -.editorWrapper > .tldraw__editor { - width: 100%; - height: 100%; -} - -@media (min-width: 768px) { - .editorWrapper[data-sidebar='true'] { - border-top-left-radius: 9px; - border-left: 1px solid var(--tla-color-border); - } -} - -.anonStylePanel { - display: flex; - flex-direction: row; - position: relative; - padding-top: 4px; - align-items: flex-start; -} - -.anonStylePanel > button:nth-of-type(1) { - margin-top: 4px; -} diff --git a/apps/dotcom/client/src/tla/components/TlaEditor/top.module.css b/apps/dotcom/client/src/tla/components/TlaEditor/top.module.css index 3979f56f25f5..d65681757a68 100644 --- a/apps/dotcom/client/src/tla/components/TlaEditor/top.module.css +++ b/apps/dotcom/client/src/tla/components/TlaEditor/top.module.css @@ -153,10 +153,6 @@ button.topLeftInputNameWidthSetter { } } -/* .inputWrapper:focus-within::after { - background-color: var(--tl-color-muted-2); -} */ - /* -------------------- Main Menu ------------------- */ .topLeftMainMenuTrigger { diff --git a/apps/dotcom/client/src/tla/components/TlaFileShareMenu/file-share-menu-primitives.tsx b/apps/dotcom/client/src/tla/components/TlaFileShareMenu/file-share-menu-primitives.tsx index 985783348a0b..ade7d90252e6 100644 --- a/apps/dotcom/client/src/tla/components/TlaFileShareMenu/file-share-menu-primitives.tsx +++ b/apps/dotcom/client/src/tla/components/TlaFileShareMenu/file-share-menu-primitives.tsx @@ -9,7 +9,7 @@ export function TlaShareMenuCopyButton({ }: { children: ReactNode onClick(): void | Promise - type?: 'primary' | 'secondary' | 'warning' + type?: 'primary' | 'secondary' }) { const [copied, setCopied] = useState(false) const [isLoading, setIsLoading] = useState(false) diff --git a/apps/dotcom/client/src/tla/components/TlaIcon/TlaIcon.tsx b/apps/dotcom/client/src/tla/components/TlaIcon/TlaIcon.tsx index b91c4e4ce74f..c67641cbdea3 100644 --- a/apps/dotcom/client/src/tla/components/TlaIcon/TlaIcon.tsx +++ b/apps/dotcom/client/src/tla/components/TlaIcon/TlaIcon.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { CSSProperties, HtmlHTMLAttributes, useLayoutEffect, useRef } from 'react' +import { CSSProperties, useLayoutEffect, useRef } from 'react' import mergedSpriteUrl from '../../../assets/0_merged_tla.svg' import styles from './icon.module.css' @@ -60,7 +60,3 @@ export function TlaIcon({ /> ) } - -export function TlaIconWrapper(props: HtmlHTMLAttributes) { - return -} diff --git a/apps/dotcom/client/src/tla/components/TlaIcon/icon.module.css b/apps/dotcom/client/src/tla/components/TlaIcon/icon.module.css index 9f1b30d2344d..918c1ee13528 100644 --- a/apps/dotcom/client/src/tla/components/TlaIcon/icon.module.css +++ b/apps/dotcom/client/src/tla/components/TlaIcon/icon.module.css @@ -13,27 +13,3 @@ .inline { display: inline-flex; } - -.iconWrapper { - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - color: inherit; - flex-shrink: 0; -} - -.iconWrapper > .icon { - height: 15px; - width: 15px; -} - -.iconWrapper[data-size='m'] { - width: 32px; -} - -.iconWrapper[data-size='m'] > .icon { - height: 20px; - width: 20px; -} diff --git a/apps/dotcom/client/src/tla/components/TlaSidebar/sidebar.module.css b/apps/dotcom/client/src/tla/components/TlaSidebar/sidebar.module.css index 7d591db711b7..b6ddaffc73d2 100644 --- a/apps/dotcom/client/src/tla/components/TlaSidebar/sidebar.module.css +++ b/apps/dotcom/client/src/tla/components/TlaSidebar/sidebar.module.css @@ -29,30 +29,6 @@ border-left: 1px solid transparent; } -.sidebarDragOverlay { - position: absolute; - inset: 0px; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - font-weight: 600; - z-index: 1000; - background-color: var(--tla-color-sidebar); - content: 'Upload .tldr files'; - opacity: 0; - animation: 0.2s fade-in 0.25s forwards; -} - -@keyframes fade-in { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - .sidebarOverlayMobile { all: unset; position: absolute; @@ -154,10 +130,6 @@ visibility: visible; } -.hoverable:has(*[data-state='open']) .collaborator { - display: none; -} - @media (hover: hover) { .hoverable:hover::after { visibility: visible; @@ -284,19 +256,6 @@ width: 64px; } -.sidebarWorkspaceButtonLabel { - /* for alignment with other buttons */ - display: block; - flex-grow: 2; - flex: 1 1 auto; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding: 0 0px; - position: relative; - z-index: 1; -} - /* Create file button */ .sidebarCreateFileButton { @@ -1035,30 +994,6 @@ } } -/* Renaming */ - -.sidebarFileLinkRenameInputWrapper { - background-color: var(--tla-color-sidebar); - outline: 1px solid var(--tl-color-primary); - outline-offset: -2px; - border-radius: 6px; -} - -.sidebarFileLinkRenameInputWrapper > div { - height: 36px; - padding: 0px 8px 0px 8px; -} - -.sidebarFileLinkRenameInput { - height: 36px; - flex: 1 1 auto; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; - z-index: 1; -} - /* Guest badge */ .sidebarFileListItemGuestBadge { @@ -1086,92 +1021,6 @@ color: var(--tla-color-text-1); } -/* Dot dev link */ - -.sidebarDotDevLink { - position: relative; - font-size: 12px; - border: 1px solid var(--tla-color-text-2); - border-radius: 2px; - margin-left: -4px; - margin-right: -6px; - margin-bottom: 8px; - padding: 2px; - display: flex; - align-items: center; - height: 36px; - justify-content: space-between; - background-color: var(--tla-color-sidebar); -} - -.sidebarDotDevLink:after { - content: ''; - position: absolute; - top: 4px; - left: -4px; - right: 4px; - bottom: -4px; - border-radius: 2px; - border: 1px dashed var(--tla-color-text-2); - z-index: -2; - pointer-events: none; -} - -.sidebarDotDevLink a { - position: relative; - height: 100%; - padding: 0px 8px; - display: flex; - align-items: center; - flex: 1; - gap: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.sidebarDotDevLink a div { - position: relative; - transform: rotate(180deg); - flex-shrink: 0; - transition: transform 3s ease-in-out 0s; -} - -.sidebarDotDevDismissButton { - background: none; - border: none; - cursor: pointer; - position: relative; - height: 28px; - width: 28px; - display: flex; - align-items: center; - justify-content: center; - color: var(--tla-color-inactive); - transition: opacity 0.2s ease-in-out; - min-width: 0; -} - -.sidebarDotDevDismissButton div { - width: 12px; - height: 12px; -} - -@media (hover: hover) { - .sidebarDotDevLink:hover a div { - transform: translate(200px, 0px) rotate(180deg); - transition: transform 8s ease-in-out 1s; - } - - .sidebarDotDevLink:has(a:hover) .sidebarDotDevDismissButton { - opacity: 0; - } - - .sidebarDotDevLink:has(a:hover):after { - background-color: var(--tla-color-hover-2); - } -} - .sidebarFileSectionTitleText { flex: 1 1 auto; white-space: nowrap; diff --git a/apps/dotcom/client/src/tla/components/WorkspaceInviteHandler.tsx b/apps/dotcom/client/src/tla/components/WorkspaceInviteHandler.tsx index 8ec7dd12b3a6..b9d6db29a9e6 100644 --- a/apps/dotcom/client/src/tla/components/WorkspaceInviteHandler.tsx +++ b/apps/dotcom/client/src/tla/components/WorkspaceInviteHandler.tsx @@ -89,6 +89,7 @@ export function WorkspaceInviteHandler() { dialogs.addDialog({ id: 'workspace-invite-sign-in', component: (props) => , + preventBackgroundClose: true, // Push, not replace: dismissing leaves the ?invite URL behind in // history, so the invite stays a real back-navigable state rather than // being erased. diff --git a/apps/dotcom/client/src/tla/components/dialogs/CreateWorkspaceDialog.tsx b/apps/dotcom/client/src/tla/components/dialogs/CreateWorkspaceDialog.tsx index 184f89820bc0..e16928b3df77 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/CreateWorkspaceDialog.tsx +++ b/apps/dotcom/client/src/tla/components/dialogs/CreateWorkspaceDialog.tsx @@ -13,7 +13,7 @@ import { defineMessages, F, useMsg } from '../../utils/i18n' import styles from './dialogs.module.css' const messages = defineMessages({ - title: { defaultMessage: 'Create workspace' }, + title: { defaultMessage: 'Create a workspace' }, name: { defaultMessage: 'Name' }, defaultName: { defaultMessage: 'New workspace' }, placeholder: { defaultMessage: 'Workspace name' }, @@ -59,11 +59,13 @@ export function CreateWorkspaceDialog({ onClose, onCreate }: CreateWorkspaceDial - -
- + +
+
+ +
{workspaceName}', + defaultMessage: + 'You have been invited to join {workspaceName}. Create a free account to continue.', }, }) @@ -203,9 +204,9 @@ function TlaEnterEmailStep({ strong: (chunks) => {chunks}, }} /> -
-
- + {/*
+
*/} + {/* */} ) : ( <> diff --git a/apps/dotcom/client/src/tla/components/dialogs/auth.module.css b/apps/dotcom/client/src/tla/components/dialogs/auth.module.css index 4942d5f780af..6b6b0de2d173 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/auth.module.css +++ b/apps/dotcom/client/src/tla/components/dialogs/auth.module.css @@ -1,16 +1,3 @@ -/* Utility Classes */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - /* Auth Dialog Container */ .authContainer { width: 450px; @@ -48,16 +35,6 @@ height: 100%; } -.authVerificationTitle { - text-align: center; - color: var(--tl-color-text-2); - font-size: 12px; - margin: 0; - line-height: 1.5; - text-wrap: balance; - font-weight: 600; -} - /* Description Text */ .authDescription { text-align: center; @@ -71,7 +48,7 @@ /* Cta button */ .authCtaButton { - height: 40px; + height: 36px; width: 100%; font-size: 12px; font-weight: 500; @@ -129,7 +106,7 @@ border: 1px solid var(--tl-color-muted-2); border-radius: var(--tl-radius-2); font-size: 12px; - height: 40px; + height: 36px; color: var(--tl-color-text-0); } @@ -169,31 +146,10 @@ text-wrap: balance; font-weight: 500; } -/* Continue Button */ -/* .authContinueButton { - width: 100%; - position: relative; - padding: 0; - font-size: 12px; - font-weight: 500; - justify-content: center; - color: var(--tl-color-text-1); - background-color: var(--tl-color-muted-2); - border: 1px solid var(--tl-color-muted-1); - border-radius: var(--tl-radius-2); - transition: background-color 0.12s ease; - margin-top: 16px; -} */ - .authContinueWithEmailButton { margin-top: var(--tl-space-4); } -.authButtonArrow { - margin-left: var(--tl-space-2); - font-size: 12px; -} - /* Checkbox */ .authCheckboxLabel { display: flex; @@ -227,10 +183,6 @@ position: relative; } -.authVerificationInput { - display: none; -} - /* OTP boxes */ .authOtpBoxes { display: grid; @@ -310,25 +262,6 @@ cursor: not-allowed; } -/* Switch Mode */ -.authSwitchMode { - text-align: center; - font-size: 12px; - color: var(--tl-color-text-2); - margin-top: var(--tl-space-2); -} - -.authSwitchModeButton { - background: none; - border: none; - color: var(--tl-color-selected); - text-decoration: underline; - cursor: pointer; - padding: 0; - font-size: 12px; - font-weight: 500; -} - .authTermsAcceptAndContinueButton { margin-top: var(--tl-space-4); } diff --git a/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css b/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css index 17dde5b4e915..a2cc23896146 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css +++ b/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css @@ -175,11 +175,19 @@ padding: 0px; } +.dialogFieldLabelRow { + height: 40px; + display: flex; + align-items: center; +} + .dialogInput { + height: 36px; border: 1px solid var(--tla-color-border); border-radius: var(--tl-radius-2); background-color: var(--tl-color-muted-0); padding: 0px var(--tl-space-3); + margin: 2px 0px; display: flex; align-items: center; justify-content: center; @@ -190,12 +198,6 @@ border: 1px solid var(--tla-color-primary); } -.dialogFooter { - display: flex; - justify-content: flex-end; - flex-shrink: 0; -} - /* ----------------- Group Settings Dialog ---------------- */ .workspaceSettingsBody { @@ -219,8 +221,9 @@ } .sectionLabel { - display: block; - margin-bottom: 8px; + height: 36px; + display: flex; + align-items: center; font-weight: 500; } @@ -307,37 +310,6 @@ color: var(--tl-color-danger); } -.modalOverlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modalContent { - background-color: white; - padding: 24px; - border-radius: 8px; - max-width: 400px; - text-align: center; -} - -.modalMessage { - margin-bottom: 16px; -} - -.modalActions { - display: flex; - gap: 8px; - justify-content: center; -} - .copyInviteLinkIconRight { right: 12.5px !important; } diff --git a/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx b/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx index 05641816d6b4..be7afee370d0 100644 --- a/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx +++ b/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx @@ -4,7 +4,6 @@ import { DropdownMenu as _DropdownMenu } from 'radix-ui' import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { - ColorSchemeMenu, TLDRAW_FILE_EXTENSION, TldrawUiIcon, TldrawUiMenuCheckboxItem, @@ -41,7 +40,6 @@ const messages = defineMessages({ accountMenu: { defaultMessage: 'User settings' }, signOut: { defaultMessage: 'Sign out' }, importFile: { defaultMessage: 'Import file…' }, - dotdev: { defaultMessage: 'Try the tldraw SDK' }, // account menu getHelp: { defaultMessage: 'User manual' }, legalSummary: { defaultMessage: 'Legal summary' }, @@ -88,12 +86,6 @@ export function SignOutMenuItem() { ) } -export function ColorThemeSubmenu() { - const editor = useMaybeEditor() - if (!editor) return null - return -} - const COLOR_SCHEMES = [ { colorScheme: 'light' as const, label: 'theme.light' }, { colorScheme: 'dark' as const, label: 'theme.dark' }, @@ -321,24 +313,6 @@ export function GiveUsFeedbackMenuItem() { ) } -export function DotDevMenuItem() { - const openAndTrack = useOpenUrlAndTrack('main-menu') - return ( - { - openAndTrack( - 'https://tldraw.dev?utm_source=dotcom&utm_medium=organic&utm_campaign=sidebar-menu', - true - ) - }} - /> - ) -} - export function LegalSummaryMenuItem() { const openAndTrack = useOpenUrlAndTrack('main-menu') return ( diff --git a/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css b/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css index e59b81deebcd..c27cdaf6ee0a 100644 --- a/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css +++ b/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css @@ -87,23 +87,10 @@ white-space: nowrap; } -.menuPopover { - padding: 40px 12px 12px 12px; - min-width: 160px; - max-width: 160px; - overflow: hidden; -} - .menuLabel + .menuInfoTrigger { margin-left: -12px; } -.menuPopoverClose { - position: absolute; - top: 0px; - right: 0px; -} - .menuDetailCentered { text-align: center; } @@ -211,10 +198,6 @@ div:has(> .menuSelectContent):not([data-radix-popper-content-wrapper]) { outline-offset: -5px; } -.menuSelectSelect:disabled { - cursor: default; -} - .menuSelectTrigger[data-disabled] .menuSelectLabel { color: var(--tla-color-text-3); } @@ -281,11 +264,6 @@ div:has(> .menuSelectContent):not([data-radix-popper-content-wrapper]) { cursor: pointer; pointer-events: all; } -.menuSwitchContainer.disabled input[type='checkbox'] { - pointer-events: none; - cursor: default; -} - .menuSwitch { --switch-h: 20px; --switch-w: calc(var(--switch-h) * 1.618); @@ -335,11 +313,11 @@ div:has(> .menuSelectContent):not([data-radix-popper-content-wrapper]) { } @media (hover: hover) { - .menuSwitchContainer:not(.disabled):hover > .menuSwitch[data-checked='false'] { + .menuSwitchContainer:hover > .menuSwitch[data-checked='false'] { background-color: var(--tla-color-inactive-hover); } - .menuSwitchContainer:not(.disabled):hover > .menuSwitch[data-checked='true'] { + .menuSwitchContainer:hover > .menuSwitch[data-checked='true'] { background-color: var(--tla-color-primary-hover); } } diff --git a/apps/dotcom/client/src/tla/styles/tla.css b/apps/dotcom/client/src/tla/styles/tla.css index 85dacad7971c..2cfb635ee31c 100644 --- a/apps/dotcom/client/src/tla/styles/tla.css +++ b/apps/dotcom/client/src/tla/styles/tla.css @@ -140,22 +140,6 @@ color: var(--tla-color-text-1); } -.tla-text_ui__section { - font-size: 12px; - line-height: 1.3; - font-weight: 500; - font-family: var(--tla-font-ui); - color: var(--tla-color-text-3); -} - -.tla-text_ui__tiny { - font-size: 6px; - line-height: 1.3; - font-weight: 800; - font-family: var(--tla-font-ui); - color: var(--tla-color-text-1); -} - .tla-text_ui__small { font-size: 11px; line-height: 1.3; @@ -186,27 +170,6 @@ text-rendering: optimizeLegibility; } -/* ---------------------- Auth ---------------------- */ - -.tla-auth_detail { - text-align: center; - color: var(--tla-color-text-3); -} - -.tla-auth_detail button { - background: none; - border: none; - display: inline; - cursor: pointer; - color: var(--tla-color-primary); -} - -.tla-file-header__share { - display: flex; - gap: 8px; - align-items: center; -} - /* --------------------- Editor --------------------- */ .tlui-layout__top { @@ -321,14 +284,6 @@ outline-offset: -5px; } -/** Checkbox */ -.tla:has(.tl-container__focused:not(.tl-container__no-focus-ring)) - :is(nav, .tlui-popover__content, .tlui-dialog__overlay) - .tla-form-checkbox:has(input:focus-visible) - > label { - outline: 2px solid var(--tl-color-focus); -} - /** Text buttons, specifically from the cookie consent */ .tla:has(.tl-container__focused:not(.tl-container__no-focus-ring)) nav a:focus-visible, .tla:has(.tl-container__focused:not(.tl-container__no-focus-ring)) diff --git a/apps/dotcom/client/src/tla/utils/app-ui-events.tsx b/apps/dotcom/client/src/tla/utils/app-ui-events.tsx index 41c89f800b2c..c1474f4e2612 100644 --- a/apps/dotcom/client/src/tla/utils/app-ui-events.tsx +++ b/apps/dotcom/client/src/tla/utils/app-ui-events.tsx @@ -1,5 +1,5 @@ import { TlaFile, TlaUser } from '@tldraw/dotcom-shared' -import { ReactNode, createContext, useContext } from 'react' +import { createContext, useContext } from 'react' import { trackEvent } from '../../utils/analytics' import { TldrawAppSessionState } from './local-session-state' @@ -84,21 +84,6 @@ const defaultEventHandler: TLAppUiContextType = trackEvent /** @internal */ export const EventsContext = createContext(defaultEventHandler) -/** @public */ -export interface TldrawAppUiEventsProviderProps { - onEvent?: TLAppUiHandler - children: ReactNode -} - -/** @public @react */ -export function TldrawAppUiEventsProvider({ onEvent, children }: TldrawAppUiEventsProviderProps) { - return ( - - {children} - - ) -} - /** @public */ export function useTldrawAppUiEvents(): TLAppUiContextType { const eventHandler = useContext(EventsContext) diff --git a/apps/dotcom/client/src/utils/shouldClearDocument.tsx b/apps/dotcom/client/src/utils/shouldClearDocument.tsx deleted file mode 100644 index 9038506d6948..000000000000 --- a/apps/dotcom/client/src/utils/shouldClearDocument.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - TLUiDialogsContextType, - TldrawUiButton, - TldrawUiButtonLabel, - TldrawUiDialogBody, - TldrawUiDialogCloseButton, - TldrawUiDialogFooter, - TldrawUiDialogHeader, - TldrawUiDialogTitle, - useTranslation, -} from 'tldraw' - -export async function shouldClearDocument(addDialog: TLUiDialogsContextType['addDialog']) { - const shouldContinue = await new Promise((resolve) => { - addDialog({ - component: ({ onClose }) => ( - { - resolve(false) - onClose() - }} - onContinue={() => { - resolve(true) - onClose() - }} - /> - ), - onClose: () => { - resolve(false) - }, - }) - }) - - return shouldContinue -} - -function ConfirmClearDialog({ onCancel, onContinue }: { onCancel(): void; onContinue(): void }) { - const msg = useTranslation() - return ( - <> - - {msg('file-system.confirm-clear.title')} - - - - {msg('file-system.confirm-clear.description')} - - - - {msg('file-system.confirm-clear.cancel')} - - onContinue()}> - {msg('file-system.confirm-clear.continue')} - - - - ) -} diff --git a/apps/dotcom/client/styles/globals.css b/apps/dotcom/client/styles/globals.css index 388c709ff2ac..ba453b2f78b5 100644 --- a/apps/dotcom/client/styles/globals.css +++ b/apps/dotcom/client/styles/globals.css @@ -132,42 +132,6 @@ a { text-decoration: none; } -.icon { - flex-shrink: 0; - width: 20px; - height: 20px; - background-color: currentColor; -} - -.scroll-light { - scrollbar-width: thin; -} -.scroll-light::-webkit-scrollbar { - display: block; - width: 8px; - height: 8px; - position: absolute; - top: 0; - left: 0; - background-color: inherit; -} -.scroll-light::-webkit-scrollbar-button { - display: none; - width: 0; - height: 10px; -} -.scroll-light::-webkit-scrollbar-thumb { - background-clip: padding-box; - width: 0; - min-height: 36px; - border: 2px solid transparent; - border-radius: 6px; - background-color: rgba(0, 0, 0, 0.25); -} -.scroll-light::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.3); -} - /* ------------------- Error Page ------------------- */ .error-page { @@ -264,12 +228,6 @@ a { text-decoration: underline; } -.board-history__restore { - position: fixed; - top: 8px; - right: 8px; -} - .board-history__load-more { display: flex; justify-content: center; @@ -327,211 +285,10 @@ a { height: 14px; } -/* ------------------ Da Share Zone ----------------- */ - .tlui-style-panel__wrapper { margin-top: 4px; } -.tlui-share-zone__connection-status { - width: 8px; - height: 100%; - position: relative; - display: flex; - align-items: center; -} - -.tlui-share-zone__connection-status::after { - content: ''; - width: 8px; - height: 8px; - background-color: currentColor; - border-radius: 100%; -} - -.tlui-share-zone__button-wrapper { - all: unset; - padding: 2px; - position: relative; - pointer-events: all; - position: relative; - cursor: pointer; -} - -.tlui-share-zone__button { - font-family: inherit; - font-size: inherit; - height: 36px; - border: 2px solid var(--tl-color-background); - border-radius: 8px; - background-color: var(--tl-color-selected); - color: var(--tl-color-selected-contrast); - text-shadow: none; - pointer-events: all; - position: relative; - font-weight: 600; -} - -.tlui-share-zone__export-button { - font-family: inherit; - font-size: inherit; - height: 36px; - border: 2px solid var(--tl-color-text); - border-radius: 8px; - background-color: var(--tl-color-text); - color: var(--tl-color-selected-contrast); - text-shadow: none; - pointer-events: all; - position: relative; - font-weight: 600; -} - -.tlui-share-zone__button::before { - position: absolute; - display: block; - content: ''; - inset: 0px; - background-color: var(--tl-color-background); - border-top-left-radius: var(--tl-radius-3); - border-bottom-right-radius: var(--tl-radius-3); - border-bottom-left-radius: var(--tl-radius-3); - z-index: -1; -} - -.tlui-share-zone__button:active { - color: var(--tl-color-selected-contrast); -} - -@media (hover: hover) { - .tlui-share-zone__button:hover { - color: var(--tl-color-selected-contrast); - } - - .tlui-share-zone__button:not(:disabled, :focus-visible):hover { - color: var(--tl-color-selected-contrast); - } -} - -.tlui-share-zone__popover { - font-size: 12px; - font-weight: inherit; - width: 200px; - max-width: 100%; - max-height: 100%; - position: relative; -} - -.tlui-share-zone__qr-code { - width: 200px; - height: 200px; - cursor: pointer; - background: none; - background-color: var(--tl-color-muted-2); - background-size: cover; - border: none; -} - -.tlui-share-zone__spinner { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.tlui-share-zone__details { - font-size: 11px; - font-weight: 400; - padding: var(--tl-space-4); - color: var(--tl-color-text-1); - line-height: 1.5; - margin: 0px; -} - -.tlui-share-zone__status { - height: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - position: relative; - left: -4px; -} - -.tlui-share-zone__status > div { - width: 8px; - height: 8px; - border-radius: 100%; -} - -/* ------------------- People Menu ------------------- */ - -/* Document Name */ - -.tlui-document-name__inner { - border-radius: calc(var(--tl-radius-2) + var(--tl-space-2)); - background-color: var(--tl-color-background); - padding: 0; - display: flex; - align-items: center; - justify-content: center; - height: 36px; - margin-top: 4px; - padding-left: 4px; - max-width: 100%; - color: var(--tl-color-text); - text-shadow: 1px 1px 0px var(--tl-color-background); - pointer-events: all; -} - -.tlui-document-name__input__wrapper { - position: relative; - max-width: calc(100% - 36px); - display: flex; - flex: auto; -} - -.tlui-document-name__input, -.tlui-document-name__text { - padding: var(--tl-space-2) var(--tl-space-3); - white-space: pre; - line-height: 24px; - min-width: 62px; -} - -.tlui-document-name__input { - position: absolute; - padding: var(--tl-space-1) var(--tl-space-3); - margin-top: 2px; - width: 100%; - border-radius: var(--tl-radius-2); - color: var(--tl-color-text); - background: transparent; -} -.tlui-document-name__input:focus { - box-shadow: inset 0px 0px 0px 1px var(--tl-color-selected); - border: none; -} - -.tlui-document-name__text { - width: 100%; - color: var(--tl-color-text); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.tlui-document-name__text__hidden { - opacity: 0; - pointer-events: none; -} - -.tlui-document-name__menu { - width: 36px; - min-width: 36px; - height: 36px; -} - .tlui-guest-icon { /* https://stackoverflow.com/a/23735769/1810018 */ -webkit-transform: translateZ(0); From 1299eeb8c7b79e84a1e79aedc3b725fa663bd319 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 17 Jun 2026 17:35:00 +0100 Subject: [PATCH 2/3] fix(dotcom): restore and polish the manage workspace dialog (#9268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to fix the empty "Manage workspace" dialog shown for the home workspace ("My workspace") that #9168 introduced — and to bring the dialog back in line with its pre-#9168 design — this PR restores and polishes the manage workspace dialog. Opening "Settings" for the home workspace returned `null` (a dimmed page with no content) while the sidebar still opened it. The home workspace is private (its group id equals the owner's user id): it can be renamed, but not shared, joined, deleted, or have its members managed. Changes: - **Home workspace** — shows a rename-only view (the name field plus a disabled invite control and the note "This is your private workspace. Create a new workspace to invite teammates.") instead of an empty dialog; the Members and Settings tabs are hidden. Resolves the Cursor Bugbot "Home workspace settings empty" flag on #9168. - **Members tab** — dropped the "({count})" suffix; the rework added it, but the pre-#9168 label was just "Members". - **Settings tab** — Regenerate / Leave / Delete are now left-aligned text-link buttons, and the negative-margin/padding alignment hacks are gone, so their labels line up with the toggle and the section headings. The shared `.menuControlRow` 4px nudge is disabled (it threw off that alignment; it's also used by the file share menu and the cookie dialog). - **Invite buttons** — apply the `big` `TlaButton` variant (added on main in #9261) to all three invite buttons so they keep a consistent size across enabled/disabled states, and use the secondary-disabled treatment for the "invites off" state, matching the home workspace's private invite button. Merged with `main` (#9261), which independently added the `big` variant and cleaned up the dialog CSS; this branch uses main's `big` rather than its own. ### Change type - [x] `bugfix` ### Test plan Covered by e2e regression tests in `ui.scenario.spec.ts`: 1. Home workspace settings render the private rename view (editable name, the private-workspace note, a disabled "Copy invite link", and no Members/Settings tabs) rather than an empty dialog. 2. The home workspace can be renamed from the dialog. 3. A shared workspace still shows the invite link and the Members/Settings tabs (existing coverage, updated for the countless "Members" label). - [ ] Unit tests - [x] End to end tests ### Release notes - Fix the empty settings dialog shown when opening settings for your personal "My workspace"; it now shows the workspace name (editable) and a note that it's private. - Polish the manage workspace dialog: a simpler "Members" label, link-style settings actions, and consistently-sized invite buttons. ### Code changes | Section | LOC change | | --------------- | ----------- | | Apps | +234 / -184 | | Tests | +56 / -1 | | Automated files | +18 / -17 | --- .../client/e2e/tests/ui.scenario.spec.ts | 57 ++- .../public/tla/locales-compiled/en.json | 26 +- apps/dotcom/client/public/tla/locales/en.json | 9 +- .../dialogs/WorkspaceSettingsDialog.tsx | 360 ++++++++++-------- .../tla/components/dialogs/dialogs.module.css | 52 ++- .../tla/components/tla-menu/menu.module.css | 6 +- 6 files changed, 308 insertions(+), 202 deletions(-) diff --git a/apps/dotcom/client/e2e/tests/ui.scenario.spec.ts b/apps/dotcom/client/e2e/tests/ui.scenario.spec.ts index 9b1e8b20bf9e..dbd4e5b6ce6a 100644 --- a/apps/dotcom/client/e2e/tests/ui.scenario.spec.ts +++ b/apps/dotcom/client/e2e/tests/ui.scenario.spec.ts @@ -240,7 +240,7 @@ test.describe('UI scenarios', () => { await expect(ownerDialog.getByPlaceholder('Workspace name')).toBeVisible() await expect(ownerDialog.getByText('Invite teammates')).toBeVisible() await expect(ownerDialog.getByRole('button', { name: 'Copy invite link' })).toBeVisible() - await expect(ownerDialog.getByText(/Members \(\d+\)/)).toBeVisible() + await expect(ownerDialog.getByRole('tab', { name: 'Members' })).toBeVisible() await expect(ownerDialog.getByText(/\(you\)/)).toBeVisible() const memberRoleSelect = ownerDialog.locator(`[id="workspace-member-role-${memberUserId}"]`) await expect(memberRoleSelect).toHaveText('Member') @@ -297,6 +297,61 @@ test.describe('UI scenarios', () => { await member.sidebar.expectFileNotVisible(fileName) }) + test('home workspace settings show a private rename dialog, not an empty one', async ({ + owner, + scenario, + }) => { + // Regression: the home workspace ("My workspace") used to render an empty dialog — the + // component returned null for it while the sidebar still opened the dialog, so the page + // just dimmed with no content. It's private: it can be renamed, but not shared or managed. + await scenario.ensureGroupsReady(owner) + await owner.sidebar.switchToHomeWorkspace() + await owner.page.getByTestId('tla-sidebar-workspace-settings').click() + + const dialog = owner.page.getByRole('dialog', { name: 'Manage workspace' }) + await expect(dialog).toBeVisible() + // Renameable: the name field is present and editable. + await expect(dialog.getByPlaceholder('Workspace name')).toBeEnabled() + // Private: a disabled invite control with an explanatory note (no shareable link). + await expect( + dialog.getByText( + 'This is your private workspace. Create a new workspace to invite teammates.' + ) + ).toBeVisible() + await expect(dialog.getByRole('button', { name: 'Copy invite link' })).toBeDisabled() + // Nothing to manage, so the dialog has no Members/Settings tabs at all. + await expect(dialog.getByRole('tab')).toHaveCount(0) + + await owner.page.keyboard.press('Escape') + }) + + test('the home workspace can be renamed from its settings dialog', async ({ + owner, + scenario, + }) => { + // Regression: the home workspace is renameable (the reason its settings dialog exists at + // all), so the rename must apply. Restore the name afterwards so later serial tests still + // see the original. + await scenario.ensureGroupsReady(owner) + await owner.sidebar.switchToHomeWorkspace() + const originalName = await owner.page.getByTestId('tla-active-workspace-name').innerText() + const newName = scenario.name('home renamed') + + await owner.page.getByTestId('tla-sidebar-workspace-settings').click() + const dialog = owner.page.getByRole('dialog', { name: 'Manage workspace' }) + await dialog.getByPlaceholder('Workspace name').fill(newName) + await owner.page.getByRole('button', { name: 'Close' }).click() + await owner.waitForMutationResolution() + await owner.sidebar.expectActiveWorkspace(newName) + + // Restore the original home workspace name. + await owner.page.getByTestId('tla-sidebar-workspace-settings').click() + await dialog.getByPlaceholder('Workspace name').fill(originalName) + await owner.page.getByRole('button', { name: 'Close' }).click() + await owner.waitForMutationResolution() + await owner.sidebar.expectActiveWorkspace(originalName) + }) + test('workspace settings rename updates labels and persists', async ({ owner, scenario }) => { const workspaceName = scenario.name('settings rename old') const newWorkspaceName = scenario.name('settings rename new') diff --git a/apps/dotcom/client/public/tla/locales-compiled/en.json b/apps/dotcom/client/public/tla/locales-compiled/en.json index e2c07241f232..2a000cea3871 100644 --- a/apps/dotcom/client/public/tla/locales-compiled/en.json +++ b/apps/dotcom/client/public/tla/locales-compiled/en.json @@ -359,6 +359,12 @@ "value": "Unable to unpublish the file." } ], + "51317dcad5": [ + { + "type": 0, + "value": "This is your private workspace. Create a new workspace to invite teammates." + } + ], "517bb809d9": [ { "type": 0, @@ -1025,20 +1031,6 @@ "value": "Copy link" } ], - "b9a68fbd56": [ - { - "type": 0, - "value": "Members (" - }, - { - "type": 1, - "value": "count" - }, - { - "type": 0, - "value": ")" - } - ], "ba0318cb81": [ { "type": 0, @@ -1393,6 +1385,12 @@ "value": " to learn more." } ], + "ef53538ae4": [ + { + "type": 0, + "value": "Members" + } + ], "ef7de3f485": [ { "type": 0, diff --git a/apps/dotcom/client/public/tla/locales/en.json b/apps/dotcom/client/public/tla/locales/en.json index 34174d92e9ee..1ce875337b2f 100644 --- a/apps/dotcom/client/public/tla/locales/en.json +++ b/apps/dotcom/client/public/tla/locales/en.json @@ -170,6 +170,9 @@ "50db1b7e1e": { "translation": "Unable to unpublish the file." }, + "51317dcad5": { + "translation": "This is your private workspace. Create a new workspace to invite teammates." + }, "517bb809d9": { "translation": "Page menu" }, @@ -471,9 +474,6 @@ "b758336699": { "translation": "Copy link" }, - "b9a68fbd56": { - "translation": "Members ({count})" - }, "ba0318cb81": { "translation": "Sign in to share" }, @@ -603,6 +603,9 @@ "ede55d955f": { "translation": "Read our cookie policy to learn more." }, + "ef53538ae4": { + "translation": "Members" + }, "ef7de3f485": { "translation": "selected" }, diff --git a/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx b/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx index 01fd349bfc4c..2dcdfadefa99 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx +++ b/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx @@ -3,8 +3,6 @@ import { Tooltip as _Tooltip } from 'radix-ui' import { useLayoutEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { - TldrawUiButton, - TldrawUiButtonLabel, TldrawUiDialogBody, TldrawUiDialogCloseButton, TldrawUiDialogHeader, @@ -85,10 +83,12 @@ export function WorkspaceSettingsDialog({ workspaceId, onClose }: WorkspaceSetti }) if (!workspaceMembership) return null - // The home workspace has no settings to manage. - if (workspaceId === app.getHomeWorkspaceId()) return null const workspace = workspaceMembership.group if (!workspace) return null + // The home workspace is a private, single-member space: it can be renamed but not shared, + // joined, or deleted, so it shows the name field and a disabled "private workspace" invite + // note — no shareable invite link, members list, or settings tabs. + const isHomeWorkspace = workspaceId === app.getHomeWorkspaceId() const currentUser = workspaceMembership.groupMembers.find( (member) => member.userId === app.getUser().id @@ -283,178 +283,204 @@ export function WorkspaceSettingsDialog({ workspaceId, onClose }: WorkspaceSetti />
-
-
- -
- {inviteLinkEnabled ? ( - + {/* The home workspace is private and can't be shared, so show a disabled invite + control with a note pointing the user to creating a shared workspace instead. */} + {isHomeWorkspace && ( +
+
+ +
+ - ) : ( - - - - )} -
+

+ +

+
+ )} + + {/* Invite link, members, and settings apply only to shared workspaces. */} + {!isHomeWorkspace && ( + <> +
+
+ +
+ {inviteLinkEnabled ? ( + + + + ) : ( + + + + )} +
- - {/* The shared tabs inset their labels by the tab padding; pull the strip out + + {/* The shared tabs inset their labels by the tab padding; pull the strip out by that amount so the first tab's label lines up with the headings, button, and member rows. The underline still spans the content width. */} -
- - - - - - - - -
+
+ + + + + + + + +
- -
-
- {members.map((member) => { - const isSelf = member.userId === app.getUser().id - // Whether this member is an owner; used to hide non-owner roles from - // viewers who can't manage the workspace. - const memberIsOwner = can(member.role, 'manageWorkspace') - // A workspace must keep at least one owner, so the last owner can't be - // removed (this also covers an owner removing themselves). - const canRemoveMember = !memberIsOwner || ownersCount > 1 - // For the same reason the last owner can't be demoted to member, so - // disable that option rather than letting the change silently no-op. - const memberRoleOptions = canRemoveMember - ? roleOptions - : roleOptions.map((option) => - option.value === 'member' ? { ...option, disabled: true } : option + +
+
+ {members.map((member) => { + const isSelf = member.userId === app.getUser().id + // Whether this member is an owner; used to hide non-owner roles from + // viewers who can't manage the workspace. + const memberIsOwner = can(member.role, 'manageWorkspace') + // A workspace must keep at least one owner, so the last owner can't be + // removed (this also covers an owner removing themselves). + const canRemoveMember = !memberIsOwner || ownersCount > 1 + // For the same reason the last owner can't be demoted to member, so + // disable that option rather than letting the change silently no-op. + const memberRoleOptions = canRemoveMember + ? roleOptions + : roleOptions.map((option) => + option.value === 'member' ? { ...option, disabled: true } : option + ) + return ( +
+
+ {member.userName.charAt(0).toUpperCase()} +
+ + {member.userName} + {isSelf ? ` (${youMsg})` : ''} + + {canManageWorkspace ? ( + + ) : ( + + ), + destructive: true, + disabled: !canRemoveMember, + tooltip: canRemoveMember ? undefined : ( + + ), + onSelect: () => + isSelf + ? openLeaveConfirmDialog() + : openRemoveConfirmDialog(member), + }, + ]} + onChange={async (value) => { + if (value === member.role) return + try { + await app.z.mutate.setWorkspaceMemberRole({ + workspaceId, + targetUserId: member.userId, + role: value, + }).client + } catch (err) { + console.error('Failed to change member role', err) + app.showMutationRejectionToast( + (err as Error).message as ZErrorCode + ) + } + }} + /> + ) : memberIsOwner ? ( + {roleLabels[member.role]} + ) : null} +
) - return ( -
-
- {member.userName.charAt(0).toUpperCase()} -
- - {member.userName} - {isSelf ? ` (${youMsg})` : ''} - - {canManageWorkspace ? ( - - ) : ( - - ), - destructive: true, - disabled: !canRemoveMember, - tooltip: canRemoveMember ? undefined : ( - - ), - onSelect: () => - isSelf ? openLeaveConfirmDialog() : openRemoveConfirmDialog(member), - }, - ]} - onChange={async (value) => { - if (value === member.role) return - try { - await app.z.mutate.setWorkspaceMemberRole({ - workspaceId, - targetUserId: member.userId, - role: value, - }).client - } catch (err) { - console.error('Failed to change member role', err) - app.showMutationRejectionToast((err as Error).message as ZErrorCode) - } - }} - /> - ) : memberIsOwner ? ( - {roleLabels[member.role]} - ) : null} -
- ) - })} -
-
-
+ })} +
+
+
- -
-
- {canManageWorkspace && ( - <> - - - - - - - - - - - - - )} - {canLeave ? ( - - - - - - ) : ( - - } - > - - + +
+
+ {canManageWorkspace && ( + <> + + + + + + + + + )} + {canLeave ? ( +
-
-
- + + ) : ( + + } + > + + + )} + {canManageWorkspace && ( + + )} +
+
+
+
+ + )}
) diff --git a/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css b/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css index a2cc23896146..51f912b1fc94 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css +++ b/apps/dotcom/client/src/tla/components/dialogs/dialogs.module.css @@ -203,8 +203,6 @@ .workspaceSettingsBody { max-width: 360px; width: calc(100vw - 48px); - /* Even-width digits for the member count (e.g. the "Members (12)" tab). */ - font-variant-numeric: tabular-nums; } .section { @@ -227,6 +225,14 @@ font-weight: 500; } +/* Help text under a section's control (e.g. the home workspace's private note). */ +.sectionHelp { + margin: 8px 0 0; + text-align: center; + text-wrap: balance; + color: var(--tla-color-text-3); +} + .membersList { display: flex; flex-direction: column; @@ -287,26 +293,42 @@ overflow-y: auto; } -/* Pull the rows out 12px so the menu buttons' built-in 12px label padding (and the - matching padded toggle row) land their labels back on the content edge, aligned - with the headings and tab labels. */ +/* The toggle row and the action links below it sit at the content edge (no inset), so + their labels line up with the section headings and tab labels above. */ .settingsPage { display: flex; flex-direction: column; - margin: 0 -12px; + gap: 8px; +} + +/* Regenerate / leave / delete render as text links rather than full menu buttons. + align-self keeps each one's hit target tight to its text, while the toggle row above + still stretches full width for its label + switch. */ +.inlineButton { + align-self: flex-start; + padding: 4px 0; + background: none; + border: none; + cursor: pointer; + font-size: 12px; + font-weight: 500; + color: var(--tla-color-text-1); + text-align: left; +} + +@media (hover: hover) { + .inlineButton:hover:not(:disabled) { + text-decoration: underline; + } } -/* The action rows reuse TldrawUiButton type="menu", whose label sits 12px in (the - button's own padding). Pad the toggle row to match so every label lines up, and - match their 40px height (the control row is otherwise 32px) so the Settings rows - are evenly spaced. */ -.settingsControl { - padding: 0 12px; - min-height: 40px; +.inlineButton:disabled { + opacity: 0.5; + cursor: not-allowed; } -/* Destructive action ("Delete workspace") — red label on the reused menu button. */ -.settingsDanger :global(.tlui-button__label) { +/* Destructive action ("Delete workspace"). */ +.inlineButtonDanger { color: var(--tl-color-danger); } diff --git a/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css b/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css index c27cdaf6ee0a..723eba1a694f 100644 --- a/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css +++ b/apps/dotcom/client/src/tla/components/tla-menu/menu.module.css @@ -26,9 +26,11 @@ height: 32px; } -.menuControlRow > *:not(:only-child):nth-last-of-type(1) { +/* This 4px nudge on the last control threw off the left alignment of the rows it's used + in (workspace settings, the file share menu, the cookie dialog), so it's disabled. */ +/* .menuControlRow > *:not(:only-child):nth-last-of-type(1) { padding-left: 4px; -} +} */ .menuInfoTriggerContainer { flex-grow: 2; From 427f064cd53234d271be8b3b067c10ce8b33559d Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Jun 2026 19:42:24 +0100 Subject: [PATCH 3/3] fix(dotcom): polish workspace settings dialog scrolling and layout (#9284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to polish the manage workspace dialog from #9268, this PR makes the tab-page scrollbar appear only when content actually overflows, sizes the dialog body so it stops jumping around, and tidies a few labels and disabled-state styles. - Scrollbar only appears when a tab page actually overflows. The container stays `overflow: hidden` and a `useScrollbarWhenScrollable` callback ref measures the content with a `ResizeObserver`, adding a `.scrollable` class that flips to `overflow: auto` only when needed. This avoids a permanent scrollbar/gutter on systems that always render scrollbars. - Sized the dialog body with a `max-height`/`min-height` so it doesn't jump around as content changes. - Renamed the sidebar workspace action label from "Settings" to "Manage". - Renamed the "Enable invite link" label to "Enable invites". - Disabled inline buttons now dim via text color instead of opacity. - Bumped the empty-state text contrast (`text-3` → `text-1`). ### Change type - [x] `improvement` ### Test plan 1. Open the manage workspace dialog from the sidebar "Manage" action. 2. With few members, confirm neither tab page shows a scrollbar or gutter. 3. Add members until a tab page overflows and confirm the scrollbar appears only then. 4. Switch between the Members and Settings tabs and confirm the dialog body height stays stable. 5. Confirm disabled inline buttons dim and the "Enable invites" label reads correctly. - [ ] Unit tests - [ ] End to end tests ### Release notes - Polish scrolling and layout in the manage workspace dialog. ### Code changes | Section | LOC change | | ------- | ---------- | | Apps | +40 / -11 | --- .../public/tla/locales-compiled/en.json | 18 ++++++++---- apps/dotcom/client/public/tla/locales/en.json | 9 ++++-- .../components/TlaSidebarWorkspaceActions.tsx | 2 +- .../dialogs/WorkspaceSettingsDialog.tsx | 28 ++++++++++++++++--- .../tla/components/dialogs/dialogs.module.css | 21 ++++++++++---- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/apps/dotcom/client/public/tla/locales-compiled/en.json b/apps/dotcom/client/public/tla/locales-compiled/en.json index 2a000cea3871..0cff19806c42 100644 --- a/apps/dotcom/client/public/tla/locales-compiled/en.json +++ b/apps/dotcom/client/public/tla/locales-compiled/en.json @@ -245,6 +245,12 @@ "value": "Editor" } ], + "34e34c43ec": [ + { + "type": 0, + "value": "Manage" + } + ], "375396d6e5": [ { "type": 0, @@ -621,12 +627,6 @@ "value": "Got it" } ], - "78faf88f4d": [ - { - "type": 0, - "value": "Enable invite link" - } - ], "797799f35e": [ { "type": 0, @@ -1013,6 +1013,12 @@ "value": "Email address" } ], + "b48becc771": [ + { + "type": 0, + "value": "Enable invites" + } + ], "b6d4223e60": [ { "type": 0, diff --git a/apps/dotcom/client/public/tla/locales/en.json b/apps/dotcom/client/public/tla/locales/en.json index 1ce875337b2f..7134e09f8e31 100644 --- a/apps/dotcom/client/public/tla/locales/en.json +++ b/apps/dotcom/client/public/tla/locales/en.json @@ -113,6 +113,9 @@ "344a7f427f": { "translation": "Editor" }, + "34e34c43ec": { + "translation": "Manage" + }, "375396d6e5": { "translation": "Remove member" }, @@ -296,9 +299,6 @@ "78e9815992": { "translation": "Got it" }, - "78faf88f4d": { - "translation": "Enable invite link" - }, "797799f35e": { "translation": "Submit an issue on GitHub" }, @@ -465,6 +465,9 @@ "b357b524e7": { "translation": "Email address" }, + "b48becc771": { + "translation": "Enable invites" + }, "b6d4223e60": { "translation": "Sign in" }, diff --git a/apps/dotcom/client/src/tla/components/TlaSidebar/components/TlaSidebarWorkspaceActions.tsx b/apps/dotcom/client/src/tla/components/TlaSidebar/components/TlaSidebarWorkspaceActions.tsx index 20b33331afc5..a0713686fb41 100644 --- a/apps/dotcom/client/src/tla/components/TlaSidebar/components/TlaSidebarWorkspaceActions.tsx +++ b/apps/dotcom/client/src/tla/components/TlaSidebar/components/TlaSidebarWorkspaceActions.tsx @@ -11,7 +11,7 @@ import styles from '../sidebar.module.css' const messages = defineMessages({ newFile: { defaultMessage: 'New file' }, - workspaceSettings: { defaultMessage: 'Settings' }, + workspaceSettings: { defaultMessage: 'Manage' }, }) /** diff --git a/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx b/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx index 2dcdfadefa99..96a168ccd81c 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx +++ b/apps/dotcom/client/src/tla/components/dialogs/WorkspaceSettingsDialog.tsx @@ -1,6 +1,6 @@ import { MAX_WORKSPACE_NAME_LENGTH, Role, ZErrorCode, can } from '@tldraw/dotcom-shared' import { Tooltip as _Tooltip } from 'radix-ui' -import { useLayoutEffect, useRef, useState } from 'react' +import { useCallback, useLayoutEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { TldrawUiDialogBody, @@ -45,12 +45,32 @@ interface WorkspaceSettingsDialogProps { onClose(): void } +// Returns a callback ref for a scroll container that shows its scrollbar only when the +// content actually overflows. The container stays `overflow: hidden` (see .tabPage) and +// gets the `.scrollable` class — flipping overflow to auto — once measurement shows the +// content is taller than the (max-height-capped) container. A ResizeObserver on the +// container and its content re-checks as members are added/removed or the window resizes. +function useScrollbarWhenScrollable() { + return useCallback((el: HTMLDivElement | null) => { + if (!el) return + const update = () => + el.classList.toggle(styles.scrollable, el.scrollHeight > el.clientHeight + 1) + update() + const observer = new ResizeObserver(update) + observer.observe(el) + for (const child of el.children) observer.observe(child) + return () => observer.disconnect() + }, []) +} + export function WorkspaceSettingsDialog({ workspaceId, onClose }: WorkspaceSettingsDialogProps) { const app = useApp() const { addDialog } = useDialogs() const [activeTab, setActiveTab] = useState('members') const [copiedInviteLink, setCopiedInviteLink] = useState(false) + const scrollableRef = useScrollbarWhenScrollable() + const namePlaceholderMsg = useMsg(messages.namePlaceholder) const ownerMsg = useMsg(messages.owner) const memberMsg = useMsg(messages.member) @@ -339,7 +359,7 @@ export function WorkspaceSettingsDialog({ workspaceId, onClose }: WorkspaceSetti
-
+
{members.map((member) => { const isSelf = member.userId === app.getUser().id @@ -424,13 +444,13 @@ export function WorkspaceSettingsDialog({ workspaceId, onClose }: WorkspaceSetti -
+
{canManageWorkspace && ( <> - +