Skip to content
Open
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
18 changes: 18 additions & 0 deletions .changeset/smart-mangos-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@conciv/ui-kit-system': patch
'@conciv/extension-testkit': patch
'@conciv/client': patch
'@conciv/app': patch
---

Widget settings view: a nested settings route under the panel with a trace-rail section nav, an
appearance section whose scheme preview tiles apply instantly, and a provenance badge that doubles as
the scope menu. Ordinary edits always write the project layer, so a value inherited from the global
layer visibly moves to this project when you change it and the global value stays put for your other
projects; the badge menu applies a value to all projects through the single `settings.applyGlobally`
server op, forks a global value back to this project, or resets to the default. Reads come from one
`settings.get` call that carries per-layer provenance and revisions, every write sends the layer
revision it expects, and a revision conflict reloads the settings and says so instead of clobbering
them. Settings changes made anywhere repaint the open widget through the settings-changed
notification on the session stream. SegmentGroup gains a `plain` variant so a consumer can render
fully custom items without the segmented-control chrome.
5 changes: 5 additions & 0 deletions .changeset/wild-pandas-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@conciv/ui-kit-system': patch
---

Add the SegmentGroup primitive: an Ark segment group whose selected indicator slides between segments through zag's indicator position variables, plus a switch thumb that now actually travels. Both snap instead of sliding under reduced motion.
1 change: 1 addition & 0 deletions apps/conciv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@conciv/ui-kit-tap": "workspace:^",
"@solid-primitives/event-listener": "^2.4.6",
"@solid-primitives/mutation-observer": "^1.2.4",
"@solid-primitives/resize-observer": "^2.2.0",
"@solid-primitives/timer": "^1.4.4",
"@tanstack/ai-client": "catalog:",
"@tanstack/hotkeys": "^0.8.0",
Expand Down
6 changes: 6 additions & 0 deletions apps/conciv/src/app/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {ExtensionInstance} from '../extension/extension-slots.js'
import type {LiveSessions} from './live-sessions.js'
import type {WarmSession} from './warm-session.js'
import type {ColorScheme} from '../lib/color-scheme.js'
import type {WidgetSettings} from '../data/widget-settings.js'

export type AppContextValue = {
rpc: RpcClient
Expand All @@ -35,6 +36,7 @@ export type AppContextValue = {
apiBase: () => string
notifyInteractive: () => void
colorScheme: Accessor<ColorScheme>
widgetSettings: WidgetSettings
}

export const AppContext = createContext<AppContextValue>()
Expand Down Expand Up @@ -128,3 +130,7 @@ export function useNotifyInteractive(): () => void {
export function useColorScheme(): Accessor<ColorScheme> {
return useAppScope('useColorScheme', (app) => app.colorScheme)
}

export function useWidgetSettings(): WidgetSettings {
return useAppScope('useWidgetSettings', (app) => app.widgetSettings)
}
2 changes: 2 additions & 0 deletions apps/conciv/src/data/app-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import type {RpcClient} from '@conciv/contract'
export type AppData = {
utils: QueryUtils
invalidateSessions: () => void
invalidateSettings: () => void
}

export function makeAppData(rpc: RpcClient, queryClient: QueryClient): AppData {
const utils = makeQueryUtils(rpc)
return {
utils,
invalidateSessions: () => void queryClient.invalidateQueries({queryKey: utils.sessions.list.key()}),
invalidateSettings: () => void queryClient.invalidateQueries({queryKey: utils.settings.get.key()}),
}
}
75 changes: 75 additions & 0 deletions apps/conciv/src/data/widget-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {createMemo, type Accessor} from 'solid-js'
import {useQuery, type QueryClient} from '@tanstack/solid-query'
import {z} from 'zod'
import {
settingsRegistry,
type SettingsLayerValue,
type SettingsRead,
type SettingsSource,
} from '@conciv/protocol/settings-types'
import type {AppData} from './app-data.js'

export const SCHEME_KEY = 'appearance.scheme'

const SchemeValueSchema = z.enum(['auto', 'light', 'dark'])
export type SchemeValue = z.infer<typeof SchemeValueSchema>
export const SCHEME_VALUES: readonly SchemeValue[] = SchemeValueSchema.options

export const FALLBACK_SCHEME: SchemeValue = SchemeValueSchema.catch('auto').parse(
settingsRegistry.entry(SCHEME_KEY)?.fallback,
)

export type SchemeLayers = {project: SettingsLayerValue; global: SettingsLayerValue}

export type SchemeSetting = {
value: SchemeValue
source: SettingsSource
layers: SchemeLayers
}

export type SettingsRevisions = {project: string; global: string}

export const ABSENT_LAYER: SettingsLayerValue = {state: 'absent', value: undefined}

const DEFAULT_LAYERS: SchemeLayers = {project: ABSENT_LAYER, global: ABSENT_LAYER}

const DEFAULT_SCHEME: SchemeSetting = {value: FALLBACK_SCHEME, source: 'default', layers: DEFAULT_LAYERS}

const NO_REVISIONS: SettingsRevisions = {project: '', global: ''}

function schemeOf(read: SettingsRead | undefined): SchemeSetting {
const view = read?.keys.find((entry) => entry.key === SCHEME_KEY)
if (!view) return DEFAULT_SCHEME
const parsed = SchemeValueSchema.safeParse(view.value)
if (!parsed.success) return {value: FALLBACK_SCHEME, source: 'default', layers: view.layers}
return {value: parsed.data, source: view.source, layers: view.layers}
}

function revisionsOf(read: SettingsRead | undefined): SettingsRevisions {
if (!read) return NO_REVISIONS
return {project: read.layers.project.revision, global: read.layers.global.revision}
}

export type WidgetSettings = {
scheme: Accessor<SchemeSetting>
revisions: Accessor<SettingsRevisions>
isLoading: Accessor<boolean>
isError: Accessor<boolean>
retry: () => void
}

export function createWidgetSettings(data: AppData, queryClient: QueryClient): WidgetSettings {
const query = useQuery(
() => data.utils.settings.get.queryOptions({retry: false}),
() => queryClient,
)
const scheme = createMemo(() => (query.isSuccess ? schemeOf(query.data) : DEFAULT_SCHEME))
const revisions = createMemo(() => (query.isSuccess ? revisionsOf(query.data) : NO_REVISIONS))
return {
scheme,
revisions,
isLoading: () => query.isLoading,
isError: () => query.isError,
retry: () => void query.refetch(),
}
}
71 changes: 71 additions & 0 deletions apps/conciv/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import { Route as QuickRouteImport } from './routes/quick'
import { Route as PanelRouteImport } from './routes/panel'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PipSessionIdRouteImport } from './routes/pip.$sessionId'
import { Route as PanelSettingsRouteImport } from './routes/panel.settings'
import { Route as PanelLatestRouteImport } from './routes/panel.latest'
import { Route as PanelConnectRouteImport } from './routes/panel.connect'
import { Route as PanelSessionIdRouteImport } from './routes/panel.$sessionId'
import { Route as PanelSettingsIndexRouteImport } from './routes/panel.settings.index'
import { Route as PanelSessionIdIndexRouteImport } from './routes/panel.$sessionId.index'
import { Route as PanelSettingsAppearanceRouteImport } from './routes/panel.settings.appearance'
import { Route as PanelSessionIdViewRouteImport } from './routes/panel.$sessionId.$view'

const QuickRoute = QuickRouteImport.update({
Expand All @@ -39,6 +42,11 @@ const PipSessionIdRoute = PipSessionIdRouteImport.update({
path: '/pip/$sessionId',
getParentRoute: () => rootRouteImport,
} as any)
const PanelSettingsRoute = PanelSettingsRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => PanelRoute,
} as any)
const PanelLatestRoute = PanelLatestRouteImport.update({
id: '/latest',
path: '/latest',
Expand All @@ -54,11 +62,21 @@ const PanelSessionIdRoute = PanelSessionIdRouteImport.update({
path: '/$sessionId',
getParentRoute: () => PanelRoute,
} as any)
const PanelSettingsIndexRoute = PanelSettingsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PanelSettingsRoute,
} as any)
const PanelSessionIdIndexRoute = PanelSessionIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PanelSessionIdRoute,
} as any)
const PanelSettingsAppearanceRoute = PanelSettingsAppearanceRouteImport.update({
id: '/appearance',
path: '/appearance',
getParentRoute: () => PanelSettingsRoute,
} as any)
const PanelSessionIdViewRoute = PanelSessionIdViewRouteImport.update({
id: '/$view',
path: '/$view',
Expand All @@ -72,9 +90,12 @@ export interface FileRoutesByFullPath {
'/panel/$sessionId': typeof PanelSessionIdRouteWithChildren
'/panel/connect': typeof PanelConnectRoute
'/panel/latest': typeof PanelLatestRoute
'/panel/settings': typeof PanelSettingsRouteWithChildren
'/pip/$sessionId': typeof PipSessionIdRoute
'/panel/$sessionId/$view': typeof PanelSessionIdViewRoute
'/panel/settings/appearance': typeof PanelSettingsAppearanceRoute
'/panel/$sessionId/': typeof PanelSessionIdIndexRoute
'/panel/settings/': typeof PanelSettingsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
Expand All @@ -84,7 +105,9 @@ export interface FileRoutesByTo {
'/panel/latest': typeof PanelLatestRoute
'/pip/$sessionId': typeof PipSessionIdRoute
'/panel/$sessionId/$view': typeof PanelSessionIdViewRoute
'/panel/settings/appearance': typeof PanelSettingsAppearanceRoute
'/panel/$sessionId': typeof PanelSessionIdIndexRoute
'/panel/settings': typeof PanelSettingsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
Expand All @@ -94,9 +117,12 @@ export interface FileRoutesById {
'/panel/$sessionId': typeof PanelSessionIdRouteWithChildren
'/panel/connect': typeof PanelConnectRoute
'/panel/latest': typeof PanelLatestRoute
'/panel/settings': typeof PanelSettingsRouteWithChildren
'/pip/$sessionId': typeof PipSessionIdRoute
'/panel/$sessionId/$view': typeof PanelSessionIdViewRoute
'/panel/settings/appearance': typeof PanelSettingsAppearanceRoute
'/panel/$sessionId/': typeof PanelSessionIdIndexRoute
'/panel/settings/': typeof PanelSettingsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
Expand All @@ -107,9 +133,12 @@ export interface FileRouteTypes {
| '/panel/$sessionId'
| '/panel/connect'
| '/panel/latest'
| '/panel/settings'
| '/pip/$sessionId'
| '/panel/$sessionId/$view'
| '/panel/settings/appearance'
| '/panel/$sessionId/'
| '/panel/settings/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
Expand All @@ -119,7 +148,9 @@ export interface FileRouteTypes {
| '/panel/latest'
| '/pip/$sessionId'
| '/panel/$sessionId/$view'
| '/panel/settings/appearance'
| '/panel/$sessionId'
| '/panel/settings'
id:
| '__root__'
| '/'
Expand All @@ -128,9 +159,12 @@ export interface FileRouteTypes {
| '/panel/$sessionId'
| '/panel/connect'
| '/panel/latest'
| '/panel/settings'
| '/pip/$sessionId'
| '/panel/$sessionId/$view'
| '/panel/settings/appearance'
| '/panel/$sessionId/'
| '/panel/settings/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
Expand Down Expand Up @@ -170,6 +204,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PipSessionIdRouteImport
parentRoute: typeof rootRouteImport
}
'/panel/settings': {
id: '/panel/settings'
path: '/settings'
fullPath: '/panel/settings'
preLoaderRoute: typeof PanelSettingsRouteImport
parentRoute: typeof PanelRoute
}
'/panel/latest': {
id: '/panel/latest'
path: '/latest'
Expand All @@ -191,13 +232,27 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PanelSessionIdRouteImport
parentRoute: typeof PanelRoute
}
'/panel/settings/': {
id: '/panel/settings/'
path: '/'
fullPath: '/panel/settings/'
preLoaderRoute: typeof PanelSettingsIndexRouteImport
parentRoute: typeof PanelSettingsRoute
}
'/panel/$sessionId/': {
id: '/panel/$sessionId/'
path: '/'
fullPath: '/panel/$sessionId/'
preLoaderRoute: typeof PanelSessionIdIndexRouteImport
parentRoute: typeof PanelSessionIdRoute
}
'/panel/settings/appearance': {
id: '/panel/settings/appearance'
path: '/appearance'
fullPath: '/panel/settings/appearance'
preLoaderRoute: typeof PanelSettingsAppearanceRouteImport
parentRoute: typeof PanelSettingsRoute
}
'/panel/$sessionId/$view': {
id: '/panel/$sessionId/$view'
path: '/$view'
Expand All @@ -222,16 +277,32 @@ const PanelSessionIdRouteWithChildren = PanelSessionIdRoute._addFileChildren(
PanelSessionIdRouteChildren,
)

interface PanelSettingsRouteChildren {
PanelSettingsAppearanceRoute: typeof PanelSettingsAppearanceRoute
PanelSettingsIndexRoute: typeof PanelSettingsIndexRoute
}

const PanelSettingsRouteChildren: PanelSettingsRouteChildren = {
PanelSettingsAppearanceRoute: PanelSettingsAppearanceRoute,
PanelSettingsIndexRoute: PanelSettingsIndexRoute,
}

const PanelSettingsRouteWithChildren = PanelSettingsRoute._addFileChildren(
PanelSettingsRouteChildren,
)

interface PanelRouteChildren {
PanelSessionIdRoute: typeof PanelSessionIdRouteWithChildren
PanelConnectRoute: typeof PanelConnectRoute
PanelLatestRoute: typeof PanelLatestRoute
PanelSettingsRoute: typeof PanelSettingsRouteWithChildren
}

const PanelRouteChildren: PanelRouteChildren = {
PanelSessionIdRoute: PanelSessionIdRouteWithChildren,
PanelConnectRoute: PanelConnectRoute,
PanelLatestRoute: PanelLatestRoute,
PanelSettingsRoute: PanelSettingsRouteWithChildren,
}

const PanelRouteWithChildren = PanelRoute._addFileChildren(PanelRouteChildren)
Expand Down
9 changes: 8 additions & 1 deletion apps/conciv/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {setShutter} from '../lib/shutter.js'
import {PanelChromeContext} from '../app/panel-chrome.js'
import {createMediaQuery, PHONE_MEDIA_QUERY} from '../lib/media-query.js'
import {applySchemeClass, createHostColorScheme} from '../lib/color-scheme.js'
import {createWidgetSettings} from '../data/widget-settings.js'
import '../styles.css'

const OPEN_DISMISSABLE_LAYER_SELECTOR = '[data-scope][data-part="content"][data-state="open"]'
Expand Down Expand Up @@ -112,7 +113,12 @@ function RootComponent() {
})

const liveSessions = makeLiveSessions()
const colorScheme = createHostColorScheme()
const hostScheme = createHostColorScheme()
const widgetSettings = createWidgetSettings(app.data, app.queryClient)
const colorScheme = createMemo(() => {
const preference = widgetSettings.scheme().value
return preference === 'auto' ? hostScheme() : preference
})

const value: AppContextValue = {
rpc: app.rpc,
Expand All @@ -137,6 +143,7 @@ function RootComponent() {
apiBase: app.apiBase,
notifyInteractive: app.notifyInteractive,
colorScheme,
widgetSettings,
}

createEffect(() => {
Expand Down
Loading
Loading