Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/sim/platform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Keep routes thin. Colocate each feature component, its CSS Module and private he

Use the shared semantic gaps from `@sim/emcn/styles/global.css` and Platform layout roles from `@sim/emcn-platform/styles/platform.css`. Title/detail stacks, sibling list items and adjacent actions each have a 2px role. The 8px row gap separates leading art from copy. Separate sibling rows with the list gap. Reserve lines for surface boundaries and data rules such as chart axes. Align a leading avatar or glyph with the first text line; center trailing metadata, badges and actions against the whole row. Align action ink with adjacent text by extending the hit area into the margin. Chat thinking, tool labels and prose share that text edge. Composer and message action groups use the same compact gap. Custom calendar, chart and summary rows use the same type and spacing. An icon that identifies a two-line notification aligns to the title; its trailing action group centers against the full row. Email thread counts and the notification trigger use the same centered inline label/count spacing. The notification trigger takes the compact secondary (Invite) button surface, and has no chevron. The tray applies control elevation once around its scroll viewport; stacked notices do not cast shadows on one another. An active popup stays above the tray, including at narrow widths. Labelled range pickers and inline ellipsis menus need accessible names, not redundant tooltips; keep tooltips on top-level icon actions.

Every Platform reading page uses `PageColumn` for the same centered, border-box column, with Home’s 1296px content maximum and fluid 16–48px gutters. `PageOverflow` extends surfaces 8px around the text edge. Home, Chat, Search, Settings, integration details, resource directories and people pages share these edges. Embedded editors and split preview canvases retain their available working space. PageColumn owns geometry only and preserves Home’s legacy theme isolation. Home and Integrations use the shared page-heading inset inside the moving content. The shell separately reserves clearance for its sidebar opener or native titlebar; responsive layout does not change either page’s own heading inset. The unpadded viewport keeps the fade at the window edge. The web sidebar opener belongs to the shell, outside the scrolling page. It stays at the expanded toolbar’s 12px top inset as the center content scrolls, including on Home and in the drawer layout. Keep the button reachable without covering page actions. Electron keeps both controls in its native titlebar. Keep the same shell inset immediately above and below the footer. Major dashboard/sidebar sections are independently spaced at 24px. Home uses the compact 3px heading gap from greeting to description, Recents to links and activity title to total. Use the 12px content gap from the Sim heading row to its input. Home widget content uses the 16px gap measured from the activity total to the chart, and from the attention, Email and Calendar labels to their first content. Header and calendar controls extend around their label line without adding height to these gaps. The ordinary 12px section gap still serves other compositions. Home has two inline alignment edges: headings and leading content share the reading-column text edge; the composer, row hover surfaces and outer control bounds extend by the shared 8px row inset. Keep Sim and the Build/Search toggle on the text edges, and align the composer and its loading placeholder with the leftmost recent and Email row on the left and the attention row and menu controls on the right. Measure the 24px dashboard gap between component boxes, including their inline hover overhang. Chart and widget boxes own that inset; the column layout allows for both insets so neighboring surfaces remain 24px apart. Vertical widget gaps use the same 24px as the description-to-Sim gap. Responsive layouts can wrap, collapse or split panels; button height, padding and type stay stable. Composer draft and placeholder keep the same type role across widths and themes; browser zoom and user text scaling still apply.
Every Platform reading page uses `PageColumn` for the same centered, border-box column, with Home’s 1296px content maximum and fluid 16–48px gutters. `PageOverflow` extends surfaces 8px around the text edge. Home, Chat, Search, Settings, integration details, resource directories and people pages share these edges. Embedded editors and split preview canvases retain their available working space. PageColumn owns geometry only and preserves Home’s legacy theme isolation. Home and Integrations use the shared page-heading inset inside the moving content. The shell separately reserves clearance for its sidebar opener or native titlebar; responsive layout does not change either page’s own heading inset. The unpadded viewport keeps the fade at the window edge. The web sidebar opener belongs to the shell, outside the scrolling page. It stays at the expanded toolbar’s 12px top inset as the center content scrolls, including on Home and in the drawer layout. Keep the button reachable without covering page actions. Electron keeps both controls in its native titlebar. Keep the same shell inset immediately above and below the footer. Major dashboard/sidebar sections are independently spaced at 24px. Home uses the compact 3px heading gap from greeting to description, Recents to links and activity title to total. Use the 12px content gap from the Sim heading row to its input. Home widget content uses the 16px gap measured from the activity total to the chart, and from the attention, Email and Calendar labels to their first content. Header and calendar controls extend around their label line without adding height to these gaps. The ordinary 12px section gap still serves other compositions. Home has two inline alignment edges: headings and leading content share the reading-column text edge; the composer, row hover surfaces and outer control bounds extend by the shared 8px row inset. Keep Sim and the Build/Search toggle on the text edges, and align the composer with the leftmost recent and Email row on the left and the attention row and menu controls on the right. Measure the 24px dashboard gap between component boxes, including their inline hover overhang. Chart and widget boxes own that inset; the column layout allows for both insets so neighboring surfaces remain 24px apart. Vertical widget gaps use the same 24px as the description-to-Sim gap. Responsive layouts can wrap, collapse or split panels; button height, padding and type stay stable. Composer draft and placeholder keep the same type role across widths and themes; browser zoom and user text scaling still apply.

Use `ControlRow` for the sidebar, identity controls, panel tabs, recent links, wrapping workspace choices and horizontal destination links. The row owns its 30px height, 8px inset and the placement and hover of a 24px action, including a leading remove button that replaces an existing glyph. Workspace names, the Workspaces launcher and its tab title are text-only. A recent without a glyph keeps its remove action at the end. A pinned workspace or chat shows its pin in the row action slot. Hover or opening the menu reveals the ellipsis in place. On touch, tapping the resting pin opens the same menu. Sort pinned rows first, applying the section’s chosen order within each group. Keep the link, tab or button semantics on the rendered element, with sibling actions outside it. Whole section headers reveal their chevron on hover; identity controls need none. Ordinary buttons, icon triggers and rows share the sidebar Home radius (`--ui-control-radius`). Use `Button` / `IconButton` for actions and Base UI triggers, passing icons and labels through their slots. Buttons that open destructive confirmation dialogs use a quiet Button with `destructive`, including page actions, Settings rows, bulk actions and modal footer actions. It shares the menu row’s neutral resting paint and red hover recipe. Final confirmation actions retain the solid destructive variant. Close, clear and dismiss × controls keep their neutral hover; their glyph alone never makes an action destructive. The composer toolbar owns its round actions. Switches and segmented controls keep their pill shape. Clicking the selected half of a two-choice segment switches to the other half. Native keyboard choice navigation still selects its destination.

Expand Down Expand Up @@ -79,6 +79,8 @@ Each `lib/data/<domain>.ts` wraps the existing hook and maps its contract shape

Views that fixtures also render stay prop-driven. A route composes `data` and a named `actions` interface, filled by session state on fixtures and by mutations on live data. A surface that exists only on live data, such as a Settings section, People or Search, reads its domain's hooks from `lib/data` directly. No fixture branch inside a view. Hide what has no backend and record it in the [parity ledger](docs/org-parity.md); never fake it. Distinguish loading, empty and failure. A list reads `status` rather than treating empty as loading.

Loading paints nothing. Static chrome (headings, labels, menus, the composer, a calendar's hour grid) renders at once; data-backed content appears when its read settles. No skeletons, shimmer, placeholder blocks or loading copy in the shell, Home or Integrations. A Home or Integrations route has no `loading.tsx`: the shell's unkeyed `<Suspense fallback={null}>` around the page keeps the current page on screen through a client navigation, and on a page load paints the sidebar before the page. One component decides which view a URL shows, and that same component renders the view's heading and Back link from the same `nuqs` state. A second reader, such as Next's `useSearchParams`, settles a few frames later and flashes the previous view's chrome.

A server read goes through `serverRequestJson`, which gives every read a deadline. A first-paint read lives in `lib/data/prefetch-<domain>.ts` under the hook's own key and `staleTime`, through `prefetchRequestJson`; it may never block or fail the page, so the client fetches what it gave up. `resolvePlatformMode` decides each server entry once: `fixture` with `SIM_PLATFORM_FIXTURES=1`, `live` with `SIM_PLATFORM_API_ORIGIN`, otherwise `none`, today's empty rendering. Load the seam with a dynamic import behind the `live` check, so fixture servers never evaluate it. The viewer's organization comes from membership on the server, read once per request by `readOrganizationSurface`. Platform URLs carry no organization.

The legacy organization layout redirects flagged organizations here through `lib/platform/routes.ts`. Link out to a legacy route only with `legacyRoutes`, which builds an absolute URL, and only to a route that mapper leaves unmapped. Linking to a mapped route makes the two surfaces redirect to each other.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
'use client'

import { type ReactNode, useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from 'react'
import {
type ReactNode,
Suspense,
useEffect,
useEffectEvent,
useLayoutEffect,
useRef,
useState,
} from 'react'
import { IconButton } from '@sim/emcn/button'
import { useCompactViewport } from '@sim/emcn/layout/use-compact-viewport'
import { OverflowText } from '@sim/emcn/overflow-text'
Expand Down Expand Up @@ -246,20 +254,26 @@ export function PlatformShell({ data, banner, children }: PlatformShellProps) {
: undefined
}
>
{location && !workspaceChat ? (
<div className={styles.pageContent} data-workspace-pane={directory ? undefined : true}>
{children}
</div>
) : scrollingPage ? (
<ScrollArea
key={pathname}
label={home ? 'Home dashboard' : 'Organization integrations'}
>
<div className={styles.pageContent}>{children}</div>
</ScrollArea>
) : (
children
)}
{/* Unkeyed, so a navigation keeps the current page until the next one is ready. */}
<Suspense fallback={null}>
{location && !workspaceChat ? (
<div
className={styles.pageContent}
data-workspace-pane={directory ? undefined : true}
>
{children}
</div>
) : scrollingPage ? (
<ScrollArea
key={pathname}
label={home ? 'Home dashboard' : 'Organization integrations'}
>
<div className={styles.pageContent}>{children}</div>
</ScrollArea>
) : (
children
)}
</Suspense>
</div>
<dialog
ref={drawerRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useScrollEdges } from '@sim/emcn/scroll-area/use-scroll-edges'
import { InlineNotice } from '@sim/emcn/status'
import styles from '@/platform/app/(shell)/home/_components/activity-chart/activity-chart.module.css'
import { RetryButton } from '@/platform/app/(shell)/home/_components/retry-button/retry-button'
import { SkeletonBlock } from '@/platform/app/(shell)/home/_components/widget-skeleton/widget-skeleton'
import type { ActivityPoint, HomeActivityState } from '@/platform/shared/home'
import { DAY_MS, formatCalendarDate } from '@/platform/shared/time'

Expand Down Expand Up @@ -143,7 +142,6 @@ export function HomeActivityChart({
/>
)}
</header>
{state.status === 'loading' && <SkeletonBlock label={`Loading ${title}`} />}
{state.status === 'error' && (
<InlineNotice
intent='warning'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import styles from '@/platform/app/(shell)/home/_components/activity-list/activi
import { HomeSection } from '@/platform/app/(shell)/home/_components/home-section/home-section'
import type { AttentionItem, EmailItem } from '@/platform/app/(shell)/home/_components/model'
import { RetryButton } from '@/platform/app/(shell)/home/_components/retry-button/retry-button'
import { WidgetSkeleton } from '@/platform/app/(shell)/home/_components/widget-skeleton/widget-skeleton'
import { platformHref } from '@/platform/lib/navigation'
import type { HomeListState } from '@/platform/shared/home'
import { formatRelativeTime } from '@/platform/shared/time'
Expand Down Expand Up @@ -88,7 +87,6 @@ export function AttentionList({ state, asOf, onRetry }: AttentionListProps) {
const search = useSearchParams()
return (
<HomeSection title='Needs your attention'>
{state.status === 'loading' && <WidgetSkeleton label='Loading what needs your attention' />}
{state.status === 'error' && (
<InlineNotice
intent='warning'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useScrollEdges } from '@sim/emcn/scroll-area/use-scroll-edges'
import { InlineNotice } from '@sim/emcn/status'
import styles from '@/platform/app/(shell)/home/_components/calendar/calendar-day.module.css'
import { RetryButton } from '@/platform/app/(shell)/home/_components/retry-button/retry-button'
import { SkeletonBlock } from '@/platform/app/(shell)/home/_components/widget-skeleton/widget-skeleton'
import type { CalendarMeeting, HomeCalendarDay } from '@/platform/shared/home'
import { formatCalendarDate, formatClockTime, formatTimeRange } from '@/platform/shared/time'

Expand Down Expand Up @@ -140,15 +139,14 @@ export function CalendarDay({ calendar, onDayChange, onReturnToToday, onRetry }:
<IconButton icon='I032' label='Next day' tooltip onClick={() => onDayChange(1)} />
</div>
</div>
{calendar.status === 'loading' && <SkeletonBlock label='Loading this day' />}
{calendar.status === 'error' && (
<InlineNotice
intent='warning'
message='This day could not load.'
action={<RetryButton onRetry={onRetry} />}
/>
)}
{calendar.status === 'ready' && (
{calendar.status !== 'error' && (
<div
key={returnCount}
ref={scrollEdges}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ConnectionPrompt } from '@sim/emcn-platform/connection-prompt'
import styles from '@/platform/app/(shell)/home/_components/connected-widget/connected-widget.module.css'
import { HomeSection } from '@/platform/app/(shell)/home/_components/home-section/home-section'
import { RetryButton } from '@/platform/app/(shell)/home/_components/retry-button/retry-button'
import { WidgetSkeleton } from '@/platform/app/(shell)/home/_components/widget-skeleton/widget-skeleton'
import type { HomeAccountView, HomeWidgetActions, HomeWidgetState } from '@/platform/shared/home'

const DISCONNECT_NOTICE =
Expand Down Expand Up @@ -113,7 +112,6 @@ export function ConnectedWidget<T>({
icon={icon}
actions={<ActionMenu label={`${title} options`} icon='I108' align='end' actions={menu} />}
>
{state.status === 'loading' && <WidgetSkeleton label={`Loading your ${noun}`} />}
{state.status === 'not_offered' && (
<EmptyState
title={`Your organization has not turned on ${noun}`}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -93,30 +93,3 @@
.column:empty {
display: none;
}
.placeholderGreeting,
.placeholderWordmark,
.placeholderComposer,
.placeholderGetStarted {
border-radius: var(--ui-control-radius);
background: var(--control-muted-fill);
}
.placeholderGreeting {
block-size: var(--spacing-56);
inline-size: 60%;
}
.placeholderWordmark {
block-size: var(--spacing-28);
inline-size: var(--spacing-40);
}
.placeholderGetStarted {
block-size: var(--spacing-132);
}
/* The real composer's first-paint heights: the Assistant's pill, the agent's field and toolbar. */
.placeholderComposer {
block-size: 2.875rem;
border-radius: var(--radius-full);
}
.placeholderComposer[data-mode="agent"] {
block-size: 6.5rem;
border-radius: var(--ui-control-radius);
}
Loading
Loading