diff --git a/apps/web/src/components/share/ShareButton.module.css b/apps/web/src/components/share/ShareButton.module.css index a61e40c538..564ccf66d6 100644 --- a/apps/web/src/components/share/ShareButton.module.css +++ b/apps/web/src/components/share/ShareButton.module.css @@ -63,7 +63,7 @@ button.primary.transparentBusy[aria-busy="true"]:disabled { mid-poll would drop focus from the button. */ button.primary:global(.is-signing) { cursor: default; background: var(--share-ink-busy); } -/* ---- soft — 88px/32px blue (updateButton / signInSecondaryAction / plain retry) ---- */ +/* ---- soft — shared 88px/32px geometry; blue sign-in / plain retry ---- */ button.soft { box-sizing: border-box; display: inline-flex; @@ -98,6 +98,24 @@ button.soft:global(.is-signing) { cursor: default; } declared this and keep the shared Button's own disabled treatment. */ button.soft.dimDisabled:disabled { opacity: 0.6; cursor: default; } +/* Update inherits soft geometry, but none of its blue interaction colors. + Disabled/loading retain the same readable green pair; :not(:disabled) + prevents hover/active from painting over those states. */ +button.update, +button.update:disabled { + background: var(--share-update-bg); + color: var(--share-update-fg); +} +button.update:hover:not(:disabled) { + background: color-mix(in srgb, var(--share-update-fg) 18%, var(--share-white)); + color: var(--share-update-fg); +} +button.update:active:not(:disabled) { + background: color-mix(in srgb, var(--share-update-fg) 24%, var(--share-white)); + color: var(--share-update-fg); +} +button.update:focus-visible { outline-color: var(--share-update-fg); } + /* ---- soft-error — CommentSyncBanner's reopened-K5 retry (red) ---- */ button.softError { background: var(--share-red-bg); diff --git a/apps/web/src/components/share/ShareButton.tsx b/apps/web/src/components/share/ShareButton.tsx index 08fce189cb..1a87ec7a5e 100644 --- a/apps/web/src/components/share/ShareButton.tsx +++ b/apps/web/src/components/share/ShareButton.tsx @@ -3,7 +3,7 @@ import type { ButtonHTMLAttributes } from 'react'; import { Button } from '@open-design/components'; import styles from './ShareButton.module.css'; -export type ShareButtonVariant = 'primary' | 'soft' | 'soft-error' | 'dark-sm'; +export type ShareButtonVariant = 'primary' | 'soft' | 'update' | 'soft-error' | 'dark-sm'; export interface ShareButtonProps extends ButtonHTMLAttributes { variant: ShareButtonVariant; @@ -36,6 +36,7 @@ export interface ShareButtonProps extends ButtonHTMLAttributes = { primary: styles.primary, soft: styles.soft, + update: `${styles.soft} ${styles.update}`, 'soft-error': `${styles.soft} ${styles.softError}`, 'dark-sm': styles.darkSm, }; diff --git a/apps/web/src/components/share/ShareTab.tsx b/apps/web/src/components/share/ShareTab.tsx index fdf2aa0ff5..fc2eac3f6f 100644 --- a/apps/web/src/components/share/ShareTab.tsx +++ b/apps/web/src/components/share/ShareTab.tsx @@ -378,7 +378,7 @@ export function ShareTab({ message={t('fileViewer.shareUpdateHint')} action={ { if (canMutatePublicShare && !viewerOnly) void planState.run(() => updateCurrentFilePublic?.() ?? Promise.resolve()); }} diff --git a/apps/web/src/styles/share-tokens.css b/apps/web/src/styles/share-tokens.css index 4db4f25c66..aa77332d86 100644 --- a/apps/web/src/styles/share-tokens.css +++ b/apps/web/src/styles/share-tokens.css @@ -28,7 +28,12 @@ --share-ink-dark-sm: var(--share-surface-dark); --share-ink-dark-sm-hover: #353535; - /* Blues — the 88px soft button (update link / sign-in-to-update / plain retry). */ + /* Update link only: ShareEntry's existing published green pair. + Reuse the product palette, not sampled screenshot colors. */ + --share-update-fg: #2F6B3F; + --share-update-bg: #E3F1E6; + + /* Blues — sign-in-to-update / plain retry (not the update action). */ --share-blue-bg: #EDF3FF; --share-blue-fg: #245FCE; --share-blue-bg-hover: #E0EAFF; diff --git a/apps/web/tests/components/share/ShareTab.copy-button.test.tsx b/apps/web/tests/components/share/ShareTab.copy-button.test.tsx index 51bfe8112f..4c310f7c00 100644 --- a/apps/web/tests/components/share/ShareTab.copy-button.test.tsx +++ b/apps/web/tests/components/share/ShareTab.copy-button.test.tsx @@ -7,6 +7,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { buildWorkspacePermissions, buildWorkspaceSeatSummary, type WorkspaceCollabContext } from '@open-design/contracts'; import { parse } from 'postcss'; import { ShareTab } from '../../../src/components/share/ShareTab'; +import { ShareButton } from '../../../src/components/share/ShareButton'; +import buttonStyles from '../../../src/components/share/ShareButton.module.css'; import { I18nProvider } from '../../../src/i18n'; import { readShareCss } from '../../helpers/read-share-css'; @@ -36,6 +38,57 @@ function props(overrides: Partial = {}): Props { }; } +describe('OPEND-3790 update-only green action', () => { + it('renders green update colors without recoloring soft actions or changing the update callback and URL', async () => { + // Apply the production stylesheet to the rendered CSS Module classes. This + // checks the actual cascade on the action, not merely a selector's presence. + const sheet = parse(readShareCss(resolve(__dirname, '../../../src/components/share/ShareButton.module.css'))); + sheet.walkRules(rule => { + rule.selector = rule.selector.replace(/:global\(([^)]+)\)/g, '$1') + .replace(/\.([a-zA-Z][\w-]*)/g, (match, name: string) => buttonStyles[name] ? `.${buttonStyles[name]}` : match); + }); + const style = document.createElement('style'); + style.textContent = sheet.toString() + readFileSync(resolve(__dirname, '../../../src/styles/primitives.css'), 'utf8'); + document.head.append(style); + try { + vi.stubGlobal('fetch', vi.fn(async () => Response.json({ + fileCount: 1, totalBytes: 12, exceedsSizeLimit: false, exclusions: [], + }))); + const update = vi.fn().mockResolvedValue(undefined); + const input = props({ projectId: 'color-fixture', filePath: 'index.html', publicationStatus: 'active', publicationFreshness: 'outdated', updateCurrentFilePublic: update }); + const view = render(<>Sign in / Retry control); + const action = screen.getByRole('button', { name: 'fileViewer.shareUpdateLink' }); + const colors = () => { + const computed = getComputedStyle(action); + expect(computed.color).toBe('rgb(47, 107, 63)'); + expect(computed.backgroundColor).toBe('rgb(227, 241, 230)'); + expect(computed.height).toBe('32px'); + expect(computed.width).toBe('88px'); + expect(computed.borderRadius).toBe('6px'); + }; + colors(); + const soft = getComputedStyle(screen.getByRole('button', { name: 'Sign in / Retry control' })); + expect(soft.color).toBe('rgb(36, 95, 206)'); + expect(soft.backgroundColor).toBe('rgb(237, 243, 255)'); + expect(update).not.toHaveBeenCalled(); + await act(async () => fireEvent.click(action)); + await waitFor(() => expect(update).toHaveBeenCalledTimes(1)); + expect(screen.getByText(input.publishedFileUrl)).toBeVisible(); + expect(input.publishCurrentFilePublic).not.toHaveBeenCalled(); + expect(input.copyPublishedFileLink).not.toHaveBeenCalled(); + for (const state of [{ publishingPublicFile: true }, { viewerOnly: true }, { streaming: true }]) { + view.rerender(); + expect(action).toBeDisabled(); + colors(); + fireEvent.click(action); + expect(update).toHaveBeenCalledTimes(1); + } + } finally { + style.remove(); + } + }); +}); + const checkPath = 'm3 8 3 3 7-7'; const linkPath = 'M10 13.5a5 5 0 0 0 7 .2l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 10.5a5 5 0 0 0-7-.2l-3 3a5 5 0 0 0 7 7l1.7-1.7';