@sybilion/uilib 1.3.91 → 1.3.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,8 +15,8 @@ function hasTabPanelContent(content) {
15
15
  return false;
16
16
  return true;
17
17
  }
18
- function PageTabs({ className, contentClassName, innerClassName, scrollbarClassName, items, tabsListProps, variant = 'link', ...props }) {
19
- return (jsxs(Tabs, { className: cn(S.root, S[`variant-${variant}`], className), variant: variant, ...props, children: [jsx(TabsList, { ...tabsListProps, className: cn(S.list, tabsListProps?.withPaddings && S.withPaddings), children: jsx(PageXScroll, { size: "sm", scrollbarClassName: cn(S.scrollbar, scrollbarClassName), innerClassName: cn(innerClassName, S.listInner), children: items.map((item, index) => (jsx(TabsTrigger, { value: item.value, children: item.label }, item.key ?? `${item.value}-${index}`))) }) }), items.map(item => hasTabPanelContent(item.content) ? (jsx(TabsContent, { value: item.value, className: cn(S.content, contentClassName), children: item.content }, item.value)) : null)] }));
18
+ function PageTabs({ className, contentClassName, innerClassName, scrollbarClassName, items, tabsListProps, variant = 'link', scrollProps, ...props }) {
19
+ return (jsxs(Tabs, { className: cn(S.root, S[`variant-${variant}`], className), variant: variant, ...props, children: [jsx(TabsList, { ...tabsListProps, className: cn(S.list, tabsListProps?.withPaddings && S.withPaddings), children: jsx(PageXScroll, { size: "sm", ...scrollProps, scrollbarClassName: cn(S.scrollbar, scrollbarClassName), innerClassName: cn(S.listInner, innerClassName), children: items.map((item, index) => (jsx(TabsTrigger, { value: item.value, children: item.label }, item.key ?? `${item.value}-${index}`))) }) }), items.map(item => hasTabPanelContent(item.content) ? (jsx(TabsContent, { value: item.value, className: cn(S.content, contentClassName), children: item.content }, item.value)) : null)] }));
20
20
  }
21
21
 
22
22
  export { PageTabs };
@@ -1,15 +1,15 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import cn from 'classnames';
3
2
  import { useState, useEffect } from 'react';
4
- import { Scroll } from '@homecode/ui';
5
3
  import S from './PageXScroll.styl.js';
4
+ import { Scroll } from '@homecode/ui';
5
+ import cn from 'classnames';
6
6
 
7
7
  const scrollSizeMap = {
8
8
  sm: 's',
9
9
  md: 'm',
10
10
  lg: 'l',
11
11
  };
12
- function PageXScroll({ children, className, innerClassName, scrollbarClassName, fullWidth = false, size = 'md', ...props }) {
12
+ function PageXScroll({ children, className, innerClassName, scrollbarClassName, fullWidth = false, size = 'md', scrollbarOffset, ...props }) {
13
13
  const [pageXPadding, setPageXPadding] = useState(24); // Default fallback
14
14
  useEffect(() => {
15
15
  if (typeof window !== 'undefined') {
@@ -25,8 +25,8 @@ function PageXScroll({ children, className, innerClassName, scrollbarClassName,
25
25
  }, []);
26
26
  return (jsx(Scroll, { x: true, size: scrollSizeMap[size], fadeSize: "xl", className: cn(className, S.root, fullWidth && S.fullWidth), innerClassName: cn(innerClassName, S.inner), xScrollbarClassName: scrollbarClassName, offset: {
27
27
  x: {
28
- before: pageXPadding,
29
- after: pageXPadding,
28
+ before: pageXPadding + (scrollbarOffset?.x?.before ?? 0),
29
+ after: pageXPadding + (scrollbarOffset?.x?.after ?? 0),
30
30
  },
31
31
  }, autoHide: true, ...props, children: children }));
32
32
  }
@@ -12,6 +12,29 @@ import S from './WorkspaceAppSwitcher.styl.js';
12
12
  function entryKey(entry) {
13
13
  return entry.id;
14
14
  }
15
+ function subtitleClassName(entry) {
16
+ return cn(S.sub, entry.subtitleTone === 'experimental' && S.subExperimental);
17
+ }
18
+ /** Overlay native app labels from host registry onto cached localStorage entries. */
19
+ function mergeDefaultAppsMetadata(apps, defaultApps) {
20
+ if (!defaultApps?.length) {
21
+ return apps;
22
+ }
23
+ const defaultsById = new Map(defaultApps.map(entry => [entry.id, entry]));
24
+ return apps.map(entry => {
25
+ const defaults = defaultsById.get(entry.id);
26
+ if (!defaults) {
27
+ return entry;
28
+ }
29
+ return {
30
+ ...entry,
31
+ displayName: defaults.displayName,
32
+ subtitle: defaults.subtitle,
33
+ subtitleTone: defaults.subtitleTone,
34
+ icon: defaults.icon ?? entry.icon,
35
+ };
36
+ });
37
+ }
15
38
  function renderIconContent(icon, iconKey) {
16
39
  if (icon != null) {
17
40
  return (jsx("span", { className: S.icon, "aria-hidden": true, children: icon }));
@@ -37,7 +60,7 @@ function useResolvedApps(appsStorageKey, defaultApps) {
37
60
  appsStorageKey !== '') {
38
61
  const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
39
62
  if (fromLs != null && fromLs.length > 0) {
40
- return fromLs;
63
+ return mergeDefaultAppsMetadata(fromLs, defaultApps);
41
64
  }
42
65
  }
43
66
  return defaultApps ?? [];
@@ -48,7 +71,9 @@ function useResolvedApps(appsStorageKey, defaultApps) {
48
71
  return;
49
72
  }
50
73
  const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
51
- setApps(fromLs != null && fromLs.length > 0 ? fromLs : (defaultApps ?? []));
74
+ setApps(fromLs != null && fromLs.length > 0
75
+ ? mergeDefaultAppsMetadata(fromLs, defaultApps)
76
+ : (defaultApps ?? []));
52
77
  }, [appsStorageKey, defaultApps]);
53
78
  return apps;
54
79
  }
@@ -62,11 +87,11 @@ function WorkspaceAppSwitcher({ pathname, onNavigate, authenticated = true, defa
62
87
  if (!displayApp) {
63
88
  return null;
64
89
  }
65
- return (jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(Button, { variant: "ghost", className: S.trigger, "aria-label": "Select workspace app", children: [jsx(IconTile, { icon: displayApp.icon, iconKey: displayApp.iconKey, accentMuted: displayApp.accentMuted, accent: displayApp.accent }), jsxs("span", { className: S.textCol, children: [jsx("span", { className: S.name, children: displayApp.displayName }), jsx("span", { className: S.sub, children: displayApp.subtitle })] }), jsx(ChevronDown, { className: S.chevron, size: 12, "aria-hidden": true })] }) }), jsx(DropdownMenuContent, { className: S.menuContent, align: "start", sideOffset: 8, elevation: "md", children: apps.map(entry => {
90
+ return (jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(Button, { variant: "ghost", className: S.trigger, "aria-label": "Select workspace app", children: [jsx(IconTile, { icon: displayApp.icon, iconKey: displayApp.iconKey, accentMuted: displayApp.accentMuted, accent: displayApp.accent }), jsxs("span", { className: S.textCol, children: [jsx("span", { className: S.name, children: displayApp.displayName }), jsx("span", { className: subtitleClassName(displayApp), children: displayApp.subtitle })] }), jsx(ChevronDown, { className: S.chevron, size: 12, "aria-hidden": true })] }) }), jsx(DropdownMenuContent, { className: S.menuContent, align: "start", sideOffset: 8, elevation: "md", children: apps.map(entry => {
66
91
  const active = current != null ? entryKey(entry) === entryKey(current) : false;
67
92
  return (jsxs(DropdownMenuItem, { className: cn(S.item, active && S.itemActive), onSelect: () => {
68
93
  onNavigate(entry.href);
69
- }, children: [jsx(IconTile, { icon: entry.icon, iconKey: entry.iconKey, accentMuted: entry.accentMuted, accent: entry.accent }), jsxs("span", { className: S.textCol, children: [jsx("span", { className: S.name, children: entry.displayName }), jsx("span", { className: S.sub, children: entry.subtitle })] })] }, entry.id));
94
+ }, children: [jsx(IconTile, { icon: entry.icon, iconKey: entry.iconKey, accentMuted: entry.accentMuted, accent: entry.accent }), jsxs("span", { className: S.textCol, children: [jsx("span", { className: S.name, children: entry.displayName }), jsx("span", { className: subtitleClassName(entry), children: entry.subtitle })] })] }, entry.id));
70
95
  }) })] }));
71
96
  }
72
97
 
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.WorkspaceAppSwitcher_trigger__s6qYT{align-items:center;background:transparent;border:none;border-radius:12px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:var(--p-2);height:auto;margin-left:var(--p-3)!important;max-width:320px;padding:var(--p-1)!important;padding-right:var(--p-3)!important;text-align:left}.WorkspaceAppSwitcher_trigger__s6qYT:hover{background-color:var(--muted)}@media (max-width:768px){.WorkspaceAppSwitcher_trigger__s6qYT{gap:0;margin-left:0!important;max-width:none;padding-right:var(--p-1)!important}.WorkspaceAppSwitcher_trigger__s6qYT .WorkspaceAppSwitcher_chevron__7kAqO,.WorkspaceAppSwitcher_trigger__s6qYT .WorkspaceAppSwitcher_textCol__K1gfI{display:none}}.WorkspaceAppSwitcher_iconTile__tVDr8{align-items:center;border-radius:10px;color:var(--fg-color);display:flex;flex-shrink:0;height:40px;justify-content:center;position:relative;width:40px}.WorkspaceAppSwitcher_iconTile__tVDr8:after,.WorkspaceAppSwitcher_iconTile__tVDr8:before{border-radius:inherit;content:\"\";display:block;height:100%;position:absolute;width:100%}.WorkspaceAppSwitcher_iconTile__tVDr8:before{background-color:var(--background)}.WorkspaceAppSwitcher_iconTile__tVDr8:after{background-color:var(--bg-color)}.WorkspaceAppSwitcher_icon__Jgw14{align-items:center;display:flex;justify-content:center;z-index:1}.WorkspaceAppSwitcher_icon__Jgw14,.WorkspaceAppSwitcher_icon__Jgw14 svg{color:var(--fg-color)!important;height:22px!important;width:22px!important}.WorkspaceAppSwitcher_textCol__K1gfI{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0}.WorkspaceAppSwitcher_name__ewMYP{color:var(--foreground);font-size:var(--text-sm);font-weight:600}.WorkspaceAppSwitcher_name__ewMYP,.WorkspaceAppSwitcher_sub__b7w1p{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.WorkspaceAppSwitcher_sub__b7w1p{color:var(--muted-foreground);font-size:var(--text-xs)}.WorkspaceAppSwitcher_menuContent__4-UNY{max-width:360px;min-width:280px}.WorkspaceAppSwitcher_item__nnufY{align-items:center;cursor:pointer;display:flex;gap:var(--p-3);outline:none;padding:var(--p-3)}.WorkspaceAppSwitcher_itemActive__3mPlO{background-color:var(--muted)}";
4
- var S = {"trigger":"WorkspaceAppSwitcher_trigger__s6qYT","textCol":"WorkspaceAppSwitcher_textCol__K1gfI","chevron":"WorkspaceAppSwitcher_chevron__7kAqO","iconTile":"WorkspaceAppSwitcher_iconTile__tVDr8","icon":"WorkspaceAppSwitcher_icon__Jgw14","name":"WorkspaceAppSwitcher_name__ewMYP","sub":"WorkspaceAppSwitcher_sub__b7w1p","menuContent":"WorkspaceAppSwitcher_menuContent__4-UNY","item":"WorkspaceAppSwitcher_item__nnufY","itemActive":"WorkspaceAppSwitcher_itemActive__3mPlO"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.WorkspaceAppSwitcher_trigger__s6qYT{align-items:center;background:transparent;border:none;border-radius:12px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:var(--p-2);height:auto;margin-left:var(--p-3)!important;max-width:320px;padding:var(--p-1)!important;padding-right:var(--p-3)!important;text-align:left}.WorkspaceAppSwitcher_trigger__s6qYT:hover{background-color:var(--muted)}@media (max-width:768px){.WorkspaceAppSwitcher_trigger__s6qYT{gap:0;margin-left:0!important;max-width:none;padding-right:var(--p-1)!important}.WorkspaceAppSwitcher_trigger__s6qYT .WorkspaceAppSwitcher_chevron__7kAqO,.WorkspaceAppSwitcher_trigger__s6qYT .WorkspaceAppSwitcher_textCol__K1gfI{display:none}}.WorkspaceAppSwitcher_iconTile__tVDr8{align-items:center;border-radius:10px;color:var(--fg-color);display:flex;flex-shrink:0;height:40px;justify-content:center;position:relative;width:40px}.WorkspaceAppSwitcher_iconTile__tVDr8:after,.WorkspaceAppSwitcher_iconTile__tVDr8:before{border-radius:inherit;content:\"\";display:block;height:100%;position:absolute;width:100%}.WorkspaceAppSwitcher_iconTile__tVDr8:before{background-color:var(--background)}.WorkspaceAppSwitcher_iconTile__tVDr8:after{background-color:var(--bg-color)}.WorkspaceAppSwitcher_icon__Jgw14{align-items:center;display:flex;justify-content:center;z-index:1}.WorkspaceAppSwitcher_icon__Jgw14,.WorkspaceAppSwitcher_icon__Jgw14 svg{color:var(--fg-color)!important;height:22px!important;width:22px!important}.WorkspaceAppSwitcher_textCol__K1gfI{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0}.WorkspaceAppSwitcher_name__ewMYP{color:var(--foreground);font-size:var(--text-sm);font-weight:600}.WorkspaceAppSwitcher_name__ewMYP,.WorkspaceAppSwitcher_sub__b7w1p{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.WorkspaceAppSwitcher_sub__b7w1p{color:var(--muted-foreground);font-size:var(--text-xs)}.WorkspaceAppSwitcher_sub__b7w1p.WorkspaceAppSwitcher_subExperimental__-zHVr{color:var(--sb-red-400);opacity:.8}.dark .WorkspaceAppSwitcher_sub__b7w1p.WorkspaceAppSwitcher_subExperimental__-zHVr{color:var(--sb-red-400)}.WorkspaceAppSwitcher_menuContent__4-UNY{max-width:360px;min-width:280px}.WorkspaceAppSwitcher_item__nnufY{align-items:center;cursor:pointer;display:flex;gap:var(--p-3);outline:none;padding:var(--p-3)}.WorkspaceAppSwitcher_itemActive__3mPlO{background-color:var(--muted)}";
4
+ var S = {"trigger":"WorkspaceAppSwitcher_trigger__s6qYT","textCol":"WorkspaceAppSwitcher_textCol__K1gfI","chevron":"WorkspaceAppSwitcher_chevron__7kAqO","iconTile":"WorkspaceAppSwitcher_iconTile__tVDr8","icon":"WorkspaceAppSwitcher_icon__Jgw14","name":"WorkspaceAppSwitcher_name__ewMYP","sub":"WorkspaceAppSwitcher_sub__b7w1p","subExperimental":"WorkspaceAppSwitcher_subExperimental__-zHVr","menuContent":"WorkspaceAppSwitcher_menuContent__4-UNY","item":"WorkspaceAppSwitcher_item__nnufY","itemActive":"WorkspaceAppSwitcher_itemActive__3mPlO"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -1,5 +1,8 @@
1
1
  import { isWorkspaceAppIconKey } from './workspaceAppIcons.js';
2
2
 
3
+ function isWorkspaceAppSubtitleTone(v) {
4
+ return v === 'default' || v === 'experimental';
5
+ }
3
6
  function parseEntry(raw) {
4
7
  if (!raw || typeof raw !== 'object') {
5
8
  return null;
@@ -12,6 +15,7 @@ function parseEntry(raw) {
12
15
  const accent = o.accent;
13
16
  const accentMuted = o.accentMuted;
14
17
  const href = o.href;
18
+ const subtitleToneRaw = o.subtitleTone;
15
19
  const prefixesRaw = o.matchPathPrefixes;
16
20
  if (typeof idRaw !== 'string' ||
17
21
  !idRaw ||
@@ -35,7 +39,7 @@ function parseEntry(raw) {
35
39
  matchPathPrefixes = prefixes;
36
40
  }
37
41
  }
38
- return {
42
+ const entry = {
39
43
  id: idRaw,
40
44
  displayName,
41
45
  subtitle,
@@ -45,6 +49,10 @@ function parseEntry(raw) {
45
49
  href,
46
50
  matchPathPrefixes,
47
51
  };
52
+ if (subtitleToneRaw != null && isWorkspaceAppSubtitleTone(subtitleToneRaw)) {
53
+ entry.subtitleTone = subtitleToneRaw;
54
+ }
55
+ return entry;
48
56
  }
49
57
  /**
50
58
  * Read validated workspace apps JSON from localStorage; returns null if missing or invalid.
@@ -1,3 +1,4 @@
1
+ import { PageXScrollProps } from '#uilib/components/ui/Page/PageXScroll/PageXScroll';
1
2
  import { TabsListProps, TabsProps, type TabsVariant } from '#uilib/components/ui/Tabs';
2
3
  export type PageTabsItem = {
3
4
  value: string;
@@ -6,13 +7,14 @@ export type PageTabsItem = {
6
7
  /** React list key when multiple items may share `value`. */
7
8
  key?: string;
8
9
  };
9
- export declare function PageTabs({ className, contentClassName, innerClassName, scrollbarClassName, items, tabsListProps, variant, ...props }: {
10
+ export declare function PageTabs({ className, contentClassName, innerClassName, scrollbarClassName, items, tabsListProps, variant, scrollProps, ...props }: {
10
11
  items: PageTabsItem[];
11
12
  tabsListProps?: TabsListProps & {
12
13
  fullWidth?: boolean;
13
14
  withPaddings?: boolean;
14
15
  };
15
16
  variant?: TabsVariant;
17
+ scrollProps?: Partial<PageXScrollProps>;
16
18
  } & TabsProps & {
17
19
  contentClassName?: string;
18
20
  innerClassName?: string;
@@ -1,11 +1,16 @@
1
1
  import { Scroll } from '@homecode/ui';
2
- interface PageXScrollProps extends Omit<React.ComponentProps<typeof Scroll>, 'size'> {
2
+ export interface PageXScrollProps extends Omit<React.ComponentProps<typeof Scroll>, 'size'> {
3
3
  size?: 'sm' | 'md' | 'lg';
4
4
  children: React.ReactNode;
5
5
  className?: string;
6
6
  innerClassName?: string;
7
7
  scrollbarClassName?: string;
8
8
  fullWidth?: boolean;
9
+ scrollbarOffset?: {
10
+ x?: {
11
+ before?: number;
12
+ after?: number;
13
+ };
14
+ };
9
15
  }
10
- export declare function PageXScroll({ children, className, innerClassName, scrollbarClassName, fullWidth, size, ...props }: PageXScrollProps): import("react/jsx-runtime").JSX.Element;
11
- export {};
16
+ export declare function PageXScroll({ children, className, innerClassName, scrollbarClassName, fullWidth, size, scrollbarOffset, ...props }: PageXScrollProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  export { WorkspaceAppSwitcher, type WorkspaceAppSwitcherProps, } from './WorkspaceAppSwitcher';
2
- export type { WorkspaceAppEntry } from './workspaceApp.types';
2
+ export type { WorkspaceAppEntry, WorkspaceAppSubtitleTone, } from './workspaceApp.types';
3
3
  export { WORKSPACE_APP_SLUG_BASE_PATH } from './workspaceApp.types';
4
4
  export { WORKSPACE_APPS_LS_KEY } from './workspaceAppsConstants';
5
5
  export { readWorkspaceAppsFromLocalStorage, writeWorkspaceAppsToLocalStorage, } from './workspaceAppsLocalStorage';
@@ -2,12 +2,15 @@ import type { ReactNode } from 'react';
2
2
  import type { WorkspaceAppIconKey } from './workspaceAppIcons';
3
3
  /** Path segment for slug apps: pathname matches `/apps/{id}` */
4
4
  export declare const WORKSPACE_APP_SLUG_BASE_PATH = "/apps";
5
+ export type WorkspaceAppSubtitleTone = 'default' | 'experimental';
5
6
  /** One surface in the workspace app switcher (serializable for localStorage). */
6
7
  export type WorkspaceAppEntry = {
7
8
  /** Slug (e.g. `my-custom-app` → `https://sybilion.io/apps/my-custom-app` via `href`). */
8
9
  id: string;
9
10
  displayName: string;
10
11
  subtitle: string;
12
+ /** Optional subtitle color tone; defaults to muted foreground. */
13
+ subtitleTone?: WorkspaceAppSubtitleTone;
11
14
  /** Custom icon for display; not persisted to localStorage. */
12
15
  icon?: ReactNode;
13
16
  /** Built-in icon lookup when `icon` is omitted (required for localStorage entries). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.91",
3
+ "version": "1.3.93",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,9 @@
1
1
  import cn from 'classnames';
2
2
 
3
- import { PageXScroll } from '#uilib/components/ui/Page/PageXScroll/PageXScroll';
3
+ import {
4
+ PageXScroll,
5
+ PageXScrollProps,
6
+ } from '#uilib/components/ui/Page/PageXScroll/PageXScroll';
4
7
  import {
5
8
  Tabs,
6
9
  TabsContent,
@@ -37,6 +40,7 @@ export function PageTabs({
37
40
  items,
38
41
  tabsListProps,
39
42
  variant = 'link',
43
+ scrollProps,
40
44
  ...props
41
45
  }: {
42
46
  items: PageTabsItem[];
@@ -45,6 +49,7 @@ export function PageTabs({
45
49
  withPaddings?: boolean;
46
50
  };
47
51
  variant?: TabsVariant;
52
+ scrollProps?: Partial<PageXScrollProps>;
48
53
  } & TabsProps & {
49
54
  contentClassName?: string;
50
55
  innerClassName?: string;
@@ -62,8 +67,9 @@ export function PageTabs({
62
67
  >
63
68
  <PageXScroll
64
69
  size="sm"
70
+ {...scrollProps}
65
71
  scrollbarClassName={cn(S.scrollbar, scrollbarClassName)}
66
- innerClassName={cn(innerClassName, S.listInner)}
72
+ innerClassName={cn(S.listInner, innerClassName)}
67
73
  >
68
74
  {items.map((item, index) => (
69
75
  <TabsTrigger
@@ -1,9 +1,8 @@
1
- import cn from 'classnames';
2
1
  import { useEffect, useState } from 'react';
3
2
 
4
- import { Scroll } from '@homecode/ui';
5
-
6
3
  import S from './PageXScroll.styl';
4
+ import { Scroll } from '@homecode/ui';
5
+ import cn from 'classnames';
7
6
 
8
7
  const scrollSizeMap = {
9
8
  sm: 's',
@@ -11,7 +10,7 @@ const scrollSizeMap = {
11
10
  lg: 'l',
12
11
  } as const;
13
12
 
14
- interface PageXScrollProps extends Omit<
13
+ export interface PageXScrollProps extends Omit<
15
14
  React.ComponentProps<typeof Scroll>,
16
15
  'size'
17
16
  > {
@@ -21,6 +20,12 @@ interface PageXScrollProps extends Omit<
21
20
  innerClassName?: string;
22
21
  scrollbarClassName?: string;
23
22
  fullWidth?: boolean;
23
+ scrollbarOffset?: {
24
+ x?: {
25
+ before?: number;
26
+ after?: number;
27
+ };
28
+ };
24
29
  }
25
30
 
26
31
  export function PageXScroll({
@@ -30,6 +35,7 @@ export function PageXScroll({
30
35
  scrollbarClassName,
31
36
  fullWidth = false,
32
37
  size = 'md',
38
+ scrollbarOffset,
33
39
  ...props
34
40
  }: PageXScrollProps) {
35
41
  const [pageXPadding, setPageXPadding] = useState(24); // Default fallback
@@ -57,8 +63,8 @@ export function PageXScroll({
57
63
  xScrollbarClassName={scrollbarClassName}
58
64
  offset={{
59
65
  x: {
60
- before: pageXPadding,
61
- after: pageXPadding,
66
+ before: pageXPadding + (scrollbarOffset?.x?.before ?? 0),
67
+ after: pageXPadding + (scrollbarOffset?.x?.after ?? 0),
62
68
  },
63
69
  }}
64
70
  autoHide
@@ -95,6 +95,13 @@
95
95
  overflow hidden
96
96
  text-overflow ellipsis
97
97
 
98
+ &.subExperimental
99
+ opacity .8
100
+ color var(--sb-red-400)
101
+
102
+ :global(.dark) &
103
+ color var(--sb-red-400)
104
+
98
105
  .menuContent
99
106
  min-width 280px
100
107
  max-width 360px
@@ -9,6 +9,7 @@ interface CssExports {
9
9
  'menuContent': string;
10
10
  'name': string;
11
11
  'sub': string;
12
+ 'subExperimental': string;
12
13
  'textCol': string;
13
14
  'trigger': string;
14
15
  }
@@ -32,6 +32,34 @@ function entryKey(entry: WorkspaceAppEntry): string {
32
32
  return entry.id;
33
33
  }
34
34
 
35
+ function subtitleClassName(entry: WorkspaceAppEntry): string {
36
+ return cn(S.sub, entry.subtitleTone === 'experimental' && S.subExperimental);
37
+ }
38
+
39
+ /** Overlay native app labels from host registry onto cached localStorage entries. */
40
+ function mergeDefaultAppsMetadata(
41
+ apps: WorkspaceAppEntry[],
42
+ defaultApps: WorkspaceAppEntry[] | undefined,
43
+ ): WorkspaceAppEntry[] {
44
+ if (!defaultApps?.length) {
45
+ return apps;
46
+ }
47
+ const defaultsById = new Map(defaultApps.map(entry => [entry.id, entry]));
48
+ return apps.map(entry => {
49
+ const defaults = defaultsById.get(entry.id);
50
+ if (!defaults) {
51
+ return entry;
52
+ }
53
+ return {
54
+ ...entry,
55
+ displayName: defaults.displayName,
56
+ subtitle: defaults.subtitle,
57
+ subtitleTone: defaults.subtitleTone,
58
+ icon: defaults.icon ?? entry.icon,
59
+ };
60
+ });
61
+ }
62
+
35
63
  function renderIconContent(
36
64
  icon: ReactNode | undefined,
37
65
  iconKey: WorkspaceAppEntry['iconKey'],
@@ -90,7 +118,7 @@ function useResolvedApps(
90
118
  ) {
91
119
  const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
92
120
  if (fromLs != null && fromLs.length > 0) {
93
- return fromLs;
121
+ return mergeDefaultAppsMetadata(fromLs, defaultApps);
94
122
  }
95
123
  }
96
124
  return defaultApps ?? [];
@@ -102,7 +130,11 @@ function useResolvedApps(
102
130
  return;
103
131
  }
104
132
  const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
105
- setApps(fromLs != null && fromLs.length > 0 ? fromLs : (defaultApps ?? []));
133
+ setApps(
134
+ fromLs != null && fromLs.length > 0
135
+ ? mergeDefaultAppsMetadata(fromLs, defaultApps)
136
+ : (defaultApps ?? []),
137
+ );
106
138
  }, [appsStorageKey, defaultApps]);
107
139
 
108
140
  return apps;
@@ -144,7 +176,9 @@ export function WorkspaceAppSwitcher({
144
176
  />
145
177
  <span className={S.textCol}>
146
178
  <span className={S.name}>{displayApp.displayName}</span>
147
- <span className={S.sub}>{displayApp.subtitle}</span>
179
+ <span className={subtitleClassName(displayApp)}>
180
+ {displayApp.subtitle}
181
+ </span>
148
182
  </span>
149
183
  <ChevronDown className={S.chevron} size={12} aria-hidden />
150
184
  </Button>
@@ -175,7 +209,9 @@ export function WorkspaceAppSwitcher({
175
209
  />
176
210
  <span className={S.textCol}>
177
211
  <span className={S.name}>{entry.displayName}</span>
178
- <span className={S.sub}>{entry.subtitle}</span>
212
+ <span className={subtitleClassName(entry)}>
213
+ {entry.subtitle}
214
+ </span>
179
215
  </span>
180
216
  </DropdownMenuItem>
181
217
  );
@@ -2,7 +2,10 @@ export {
2
2
  WorkspaceAppSwitcher,
3
3
  type WorkspaceAppSwitcherProps,
4
4
  } from './WorkspaceAppSwitcher';
5
- export type { WorkspaceAppEntry } from './workspaceApp.types';
5
+ export type {
6
+ WorkspaceAppEntry,
7
+ WorkspaceAppSubtitleTone,
8
+ } from './workspaceApp.types';
6
9
  export { WORKSPACE_APP_SLUG_BASE_PATH } from './workspaceApp.types';
7
10
  export { WORKSPACE_APPS_LS_KEY } from './workspaceAppsConstants';
8
11
  export {
@@ -5,12 +5,16 @@ import type { WorkspaceAppIconKey } from './workspaceAppIcons';
5
5
  /** Path segment for slug apps: pathname matches `/apps/{id}` */
6
6
  export const WORKSPACE_APP_SLUG_BASE_PATH = '/apps';
7
7
 
8
+ export type WorkspaceAppSubtitleTone = 'default' | 'experimental';
9
+
8
10
  /** One surface in the workspace app switcher (serializable for localStorage). */
9
11
  export type WorkspaceAppEntry = {
10
12
  /** Slug (e.g. `my-custom-app` → `https://sybilion.io/apps/my-custom-app` via `href`). */
11
13
  id: string;
12
14
  displayName: string;
13
15
  subtitle: string;
16
+ /** Optional subtitle color tone; defaults to muted foreground. */
17
+ subtitleTone?: WorkspaceAppSubtitleTone;
14
18
  /** Custom icon for display; not persisted to localStorage. */
15
19
  icon?: ReactNode;
16
20
  /** Built-in icon lookup when `icon` is omitted (required for localStorage entries). */
@@ -1,6 +1,13 @@
1
- import type { WorkspaceAppEntry } from './workspaceApp.types';
1
+ import type {
2
+ WorkspaceAppEntry,
3
+ WorkspaceAppSubtitleTone,
4
+ } from './workspaceApp.types';
2
5
  import { isWorkspaceAppIconKey } from './workspaceAppIcons';
3
6
 
7
+ function isWorkspaceAppSubtitleTone(v: unknown): v is WorkspaceAppSubtitleTone {
8
+ return v === 'default' || v === 'experimental';
9
+ }
10
+
4
11
  function parseEntry(raw: unknown): WorkspaceAppEntry | null {
5
12
  if (!raw || typeof raw !== 'object') {
6
13
  return null;
@@ -13,6 +20,7 @@ function parseEntry(raw: unknown): WorkspaceAppEntry | null {
13
20
  const accent = o.accent;
14
21
  const accentMuted = o.accentMuted;
15
22
  const href = o.href;
23
+ const subtitleToneRaw = o.subtitleTone;
16
24
  const prefixesRaw = o.matchPathPrefixes;
17
25
 
18
26
  if (
@@ -43,7 +51,7 @@ function parseEntry(raw: unknown): WorkspaceAppEntry | null {
43
51
  }
44
52
  }
45
53
 
46
- return {
54
+ const entry: WorkspaceAppEntry = {
47
55
  id: idRaw,
48
56
  displayName,
49
57
  subtitle,
@@ -53,6 +61,12 @@ function parseEntry(raw: unknown): WorkspaceAppEntry | null {
53
61
  href,
54
62
  matchPathPrefixes,
55
63
  };
64
+
65
+ if (subtitleToneRaw != null && isWorkspaceAppSubtitleTone(subtitleToneRaw)) {
66
+ entry.subtitleTone = subtitleToneRaw;
67
+ }
68
+
69
+ return entry;
56
70
  }
57
71
 
58
72
  /**