Skip to content

Commit c996233

Browse files
committed
Merge remote-tracking branch 'origin/feat/org' into codex/pr-7477
2 parents 90b5bd8 + 4f1e635 commit c996233

154 files changed

Lines changed: 3690 additions & 732 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/rules/emcn-components.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
3232
- **`ChipDatePicker`** — chip-styled date field.
3333
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
3434
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
35+
- **`useScrollEdges` + `scrollFadeClass` / `scrollFadeAttributes`** — the canonical scroll-region edge treatment. The hook reports which edges hide content (tracking scroll and resizes; pass the element itself, held in state, when the region mounts after its owner, e.g. inside a Radix portal); the class and attributes fade a fixed 12px band at an active edge only, so a list that fits or sits at its top is never fogged. A floating control over the top edge sets `--scroll-fade-inset` to its height. A region that scrolls sideways (a tab row, a chip strip) uses `useScrollEdges(ref, { axis: 'x' })` with `scrollFadeXClass`; the attributes helper is shared. Any divider beside the region belongs to the neighboring block (`border-b` above, `border-t` below), never to the masked element, and shows only while that edge is active. Never hand-roll a `mask-image` gradient for a scroll region.
3536
- **`OverflowText`** — the canonical single-line overflow treatment for read-only human labels and titles. It owns `min-w-0`, fade-only clipping (never an ellipsis), the conditional 18px edge mask, and the full-value floating tooltip; consumers pass only layout/typography through `className`. `overflowTextClipClass` and `overflowTextFadeClass` are the complete base/faded treatments for the rare component that must own measurement itself; never pair either with `truncate`, `text-ellipsis`, or hover-time mask removal. Use `DropdownMenuItemLabel` for a menu label beside icons, checks, or actions. A non-editable `Combobox` passes the full visual value through `overlayLabel`; the combobox owns the visual overlay's fade and keeps its one accessible tooltip on the interactive layer. Keep ordinary `truncate` only for editable values, code/log/path content, dense or virtualized grids, and rich composite content that cannot supply a plain tooltip label. Multiline copy uses an intentional `line-clamp-*` treatment instead.
3637

3738
## Modal keyboard defaults

‎.claude/rules/sim-styling.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ Use `DropdownMenuItemLabel` for a human label beside menu icons, checks, shortcu
6060

6161
Do not apply the fade universally to editable or mirrored input values, code, logs, paths, filenames that use intentional middle truncation, dense or virtualized grids, or a composite container that also holds icons/actions. Those keep their purpose-built overflow behavior. Multiline copy uses an intentional `line-clamp-*` treatment.
6262

63+
## Scroll Edges
64+
65+
A scroll region that can hide rows past an edge uses `useScrollEdges` with `scrollFadeClass` + `scrollFadeAttributes` from `@sim/emcn`: a 12px fade at an edge only while content is hidden beyond it, never at rest. The region's baseline padding lives on the scroll box itself (so rows pass through it under the fade), and the divider at that edge is drawn by the neighboring block, conditional on the same edge. Never hand-roll a `mask-image` gradient or a `scrollTop > 0` effect for this.
66+
6367
## Font Weight
6468

6569
Three steps, Tailwind's stock scale, nothing else: **`font-normal` (400)**, **`font-medium` (500)**, **`font-semibold` (600)**. 400 is the document default, so body text, chip labels, sidebar items, and headings carry **no weight class at all** — they inherit. Reach for a class only to step *up* from body.
@@ -70,7 +74,7 @@ Headings inherit their weight. Tailwind preflight resets `h1`–`h6` to `font-we
7074

7175
## Color Tokens
7276

73-
Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text-icon`; neutral borders and dividers `--border` (`--border-1` and `--border-muted` are legacy aliases resolving to it; `--divider` is retired); surfaces `--surface-5` (light) / `--surface-4` (dark); active row `--surface-active`; error `--text-error`. No focus rings on chip surfaces.
77+
Value text `--text-body`; muted/placeholder/labels `--text-muted`; icons `--text-icon`; progress and completion (a checked step, a done state) `--brand-blue` — `--selection` stays the interactive highlight; neutral borders and dividers `--border` (`--border-1` and `--border-muted` are legacy aliases resolving to it; `--divider` is retired); surfaces `--surface-5` (light) / `--surface-4` (dark); active row `--surface-active`; error `--text-error`. No focus rings on chip surfaces.
7478

7579
### Line weight
7680

‎apps/desktop/e2e/smoke.spec.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { _electron as electron, expect, test } from '@playwright/test'
1010
const DESKTOP_DIR = fileURLToPath(new URL('..', import.meta.url))
1111

1212
const PAGES: Record<string, string> = {
13-
'/workspace': `<!doctype html><html><head><title>Sim Fixture</title></head><body>
13+
'/home': `<!doctype html><html><head><title>Sim Fixture</title></head><body>
1414
<h1 id="app">fixture-app</h1>
1515
<button id="internal-blank" onclick="window.open('/workspace/two', '_blank')">internal</button>
1616
<button id="external-blank" onclick="window.open('https://docs.sim.ai/x', '_blank')">external</button>
@@ -82,7 +82,7 @@ test.describe('desktop shell smoke', () => {
8282
app = await launchApp(origin)
8383
const window = await app.firstWindow()
8484
await expect(window.locator('#app')).toHaveText('fixture-app')
85-
expect(window.url()).toBe(`${origin}/workspace`)
85+
expect(window.url()).toBe(`${origin}/home`)
8686
})
8787

8888
test('internal window.open creates an independent full Sim window', async () => {
@@ -150,7 +150,7 @@ test.describe('desktop shell smoke', () => {
150150
app.evaluate(() => (globalThis as { __openedExternal?: string[] }).__openedExternal)
151151
)
152152
.toEqual(['https://docs.sim.ai/navigation'])
153-
expect(window.url()).toBe(`${origin}/workspace`)
153+
expect(window.url()).toBe(`${origin}/home`)
154154
})
155155

156156
test('unreachable origin shows the bundled offline page', async () => {

‎apps/desktop/src/main/app-routes.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ describe('app routes', () => {
55
it('derives the new-chat route from the last workspace route', () => {
66
expect(newChatRoute('/workspace/ws1/w/wf2')).toBe('/workspace/ws1/home')
77
expect(newChatRoute('/workspace/ws1/home?resource=r1')).toBe('/workspace/ws1/home')
8-
expect(newChatRoute('/account')).toBe('/workspace')
9-
expect(newChatRoute(undefined)).toBe('/workspace')
10-
expect(newChatRoute('//evil.example')).toBe('/workspace')
8+
expect(newChatRoute('/account')).toBe('/home')
9+
expect(newChatRoute(undefined)).toBe('/home')
10+
expect(newChatRoute('//evil.example')).toBe('/home')
1111
})
1212

1313
it('derives the settings route from the last workspace route', () => {
1414
expect(settingsRoute('/workspace/ws1/w/wf2')).toBe('/workspace/ws1/settings/desktop')
15-
expect(settingsRoute('/account')).toBe('/workspace')
16-
expect(settingsRoute(undefined)).toBe('/workspace')
17-
expect(settingsRoute('//evil.example')).toBe('/workspace')
15+
expect(settingsRoute('/account')).toBe('/home')
16+
expect(settingsRoute(undefined)).toBe('/home')
17+
expect(settingsRoute('//evil.example')).toBe('/home')
1818
})
1919
})

‎apps/desktop/src/main/app-routes.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { isSafeInternalPath } from '@/main/config'
1010
* do with the tray, and the tray can be absent entirely.
1111
*/
1212

13+
/**
14+
* The web app's signed-in entry. It resolves to the organization the user belongs
15+
* to, or to their workspaces, so the shell never has to know which applies.
16+
*/
17+
export const APP_ENTRY_ROUTE = '/home'
18+
1319
/** Workspace id from the last visited route, or null when it carries none. */
1420
function workspaceIdFromRoute(lastRoute: string | undefined): string | null {
1521
if (isSafeInternalPath(lastRoute)) {
@@ -23,19 +29,19 @@ function workspaceIdFromRoute(lastRoute: string | undefined): string | null {
2329

2430
/**
2531
* Route for "New Chat": the home (chat) surface of the workspace the user was
26-
* last in, falling back to the workspace picker redirect when the last route
27-
* carries no workspace.
32+
* last in, falling back to the app entry when the last route carries no
33+
* workspace.
2834
*/
2935
export function newChatRoute(lastRoute: string | undefined): string {
3036
const workspaceId = workspaceIdFromRoute(lastRoute)
31-
return workspaceId ? `/workspace/${workspaceId}/home` : '/workspace'
37+
return workspaceId ? `/workspace/${workspaceId}/home` : APP_ENTRY_ROUTE
3238
}
3339

3440
/**
3541
* Route for "Settings…": the Sim app's settings surface for the workspace the
36-
* user was last in, falling back to the workspace picker redirect.
42+
* user was last in, falling back to the app entry.
3743
*/
3844
export function settingsRoute(lastRoute: string | undefined): string {
3945
const workspaceId = workspaceIdFromRoute(lastRoute)
40-
return workspaceId ? `/workspace/${workspaceId}/settings/desktop` : '/workspace'
46+
return workspaceId ? `/workspace/${workspaceId}/settings/desktop` : APP_ENTRY_ROUTE
4147
}

‎apps/desktop/src/main/session-lifecycle.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ describe('decideStartRoute', () => {
7575
})
7676

7777
it('falls back to /workspace for missing, unsafe, or auth-surface last routes', () => {
78-
expect(decideStartRoute(undefined)).toBe('/workspace')
79-
expect(decideStartRoute('//evil.example')).toBe('/workspace')
80-
expect(decideStartRoute('/login')).toBe('/workspace')
78+
expect(decideStartRoute(undefined)).toBe('/home')
79+
expect(decideStartRoute('//evil.example')).toBe('/home')
80+
expect(decideStartRoute('/login')).toBe('/home')
8181
})
8282
})
8383

@@ -94,11 +94,11 @@ describe('resolveStartRoute', () => {
9494
)
9595
})
9696

97-
it('falls back to the workspace picker after confirmed access denial', async () => {
97+
it('falls back to the app entry after confirmed access denial', async () => {
9898
const session = sessionWithResponse(403, { error: 'Workspace access denied' })
9999

100100
await expect(resolveStartRoute(session, APP, '/workspace/revoked/chat/c1')).resolves.toBe(
101-
'/workspace'
101+
'/home'
102102
)
103103
})
104104

‎apps/desktop/src/main/session-lifecycle.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
completeAccountDataTeardown,
88
waitForAccountDataMutations,
99
} from '@/main/account-data-generation'
10+
import { APP_ENTRY_ROUTE } from '@/main/app-routes'
1011
import { isSafeInternalPath } from '@/main/config'
1112
import { isAuthSurfacePath, openExternalSafe } from '@/main/navigation'
1213
import type { EventRecorder } from '@/main/observability'
@@ -60,14 +61,14 @@ export function isLogoutNavigation(rawUrl: string, appOrigin: string): boolean {
6061

6162
/**
6263
* Picks the route to load at launch: the last visited route (when safe and
63-
* not itself an auth surface), falling back to /workspace. A signed-out
64+
* not itself an auth surface), falling back to the app entry. A signed-out
6465
* partition is handled by the web app's own login redirect.
6566
*/
6667
export function decideStartRoute(lastRoute: string | undefined): string {
6768
if (lastRoute && isSafeInternalPath(lastRoute) && !isAuthSurfacePath(lastRoute)) {
6869
return lastRoute
6970
}
70-
return '/workspace'
71+
return APP_ENTRY_ROUTE
7172
}
7273

7374
function workspaceIdFromRoute(route: string): string | null {
@@ -110,8 +111,8 @@ export async function resolveStartRoute(
110111
}
111112
)
112113
if (response.status === 403) {
113-
logger.info('Saved workspace route is no longer accessible; opening workspace picker')
114-
return '/workspace'
114+
logger.info('Saved workspace route is no longer accessible; opening the app entry')
115+
return APP_ENTRY_ROUTE
115116
}
116117
return route
117118
} catch {

‎apps/sim/app/(auth)/auth-redirect.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('resolvePostSignupDestination', () => {
3232
it('never routes to verify when no mail provider is configured', () => {
3333
expect(
3434
resolvePostSignupDestination({ emailVerificationEnabled: false, redirectUrl: '' })
35-
).toEqual({ kind: 'workspace' })
35+
).toEqual({ kind: 'entry' })
3636
})
3737

3838
it('preserves the callback URL when verification is not enforceable', () => {

‎apps/sim/app/(auth)/auth-redirect.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { APP_ENTRY_PATH } from '@/lib/navigation/paths'
2+
13
/**
24
* Where the user goes once authentication finishes, carried across the login →
35
* signup → verify hops. Written only after `validateCallbackUrl` accepts it, and
@@ -8,19 +10,22 @@ export const POST_AUTH_REDIRECT_STORAGE_KEY = 'postAuthRedirectUrl'
810
/** Route the verify hop lives at, entered only from signup. */
911
export const VERIFY_FROM_SIGNUP_ROUTE = '/verify?fromSignup=true'
1012

11-
/** Default post-auth destination when no callback URL was carried in. */
12-
export const DEFAULT_POST_AUTH_ROUTE = '/workspace'
13+
/**
14+
* Default post-auth destination when no callback URL was carried in: the app
15+
* entry, which resolves to the viewer's organization or their workspaces.
16+
*/
17+
export const DEFAULT_POST_AUTH_ROUTE = APP_ENTRY_PATH
1318

1419
/**
1520
* Where a successful email signup goes next.
1621
* - `verify`: the verification hop, which owns the post-auth redirect from there
1722
* - `redirect`: the validated callback URL the visitor arrived with
18-
* - `workspace`: the default destination
23+
* - `entry`: the default destination, {@link DEFAULT_POST_AUTH_ROUTE}
1924
*/
2025
export type PostSignupDestination =
2126
| { kind: 'verify' }
2227
| { kind: 'redirect'; url: string }
23-
| { kind: 'workspace' }
28+
| { kind: 'entry' }
2429

2530
interface PostSignupDestinationParams {
2631
/** The server-derived effective flag — verification enabled AND deliverable. */
@@ -40,7 +45,7 @@ export function resolvePostSignupDestination({
4045
redirectUrl,
4146
}: PostSignupDestinationParams): PostSignupDestination {
4247
if (emailVerificationEnabled) return { kind: 'verify' }
43-
return redirectUrl ? { kind: 'redirect', url: redirectUrl } : { kind: 'workspace' }
48+
return redirectUrl ? { kind: 'redirect', url: redirectUrl } : { kind: 'entry' }
4449
}
4550

4651
/** The raw redirect-carrying params, as read from a URL on client or server. */

‎apps/sim/app/(auth)/components/social-login-buttons.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createLogger } from '@sim/logger'
66
import { getErrorMessage } from '@sim/utils/errors'
77
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
88
import { client } from '@/lib/auth/auth-client'
9+
import { DEFAULT_POST_AUTH_ROUTE } from '@/app/(auth)/auth-redirect'
910
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
1011

1112
const logger = createLogger('SocialLoginButtons')
@@ -22,7 +23,7 @@ export function SocialLoginButtons({
2223
githubAvailable,
2324
googleAvailable,
2425
microsoftAvailable,
25-
callbackURL = '/workspace',
26+
callbackURL = DEFAULT_POST_AUTH_ROUTE,
2627
children,
2728
}: SocialLoginButtonsProps) {
2829
const [isGithubLoading, setIsGithubLoading] = useState(false)

0 commit comments

Comments
 (0)