@butinapp/ui 0.1.0 → 0.1.1

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.
@@ -8,8 +8,9 @@ import { Checkbox } from './checkbox.js';
8
8
  import { formatRows, normalizeTablePrefs, selection as sel, sortRows } from './data-table-model.js';
9
9
  import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './dropdown-menu.js';
10
10
  import { Input } from './input.js';
11
- // First entry is the default selection (see pageSize seeding below).
12
11
  const DEFAULT_PAGE_SIZES = [15, 25, 50, 100];
12
+ // The page size a table opens at when nothing is saved (see pageSize seeding below).
13
+ const DEFAULT_PAGE_SIZE = 25;
13
14
  // Generic, headless, theme-portable table. TanStack owns sorting/column-order/visibility/pagination;
14
15
  // selection (external Set) and grouping (manual collapsible sections over the sorted rows) are layered
15
16
  // on top so grouping stays fully controllable. Pure — prefs in via props, export out via callback.
@@ -45,7 +46,8 @@ export const DataTable = ({ columns, rows, getRowId, selection, onSelectionChang
45
46
  sorting: (seed.sort ? [{ id: seed.sort.key, desc: seed.sort.dir === 'desc' }] : []),
46
47
  columnOrder: (seed.columnOrder ?? []),
47
48
  columnVisibility: (seed.columnVisibility ?? {}),
48
- pageSize: seed.pageSize ?? pageSizes[0] ?? 15
49
+ pageSize: seed.pageSize ??
50
+ (pageSizes.includes(DEFAULT_PAGE_SIZE) ? DEFAULT_PAGE_SIZE : (pageSizes[0] ?? DEFAULT_PAGE_SIZE))
49
51
  }), [seed, pageSizes]);
50
52
  const [sorting, setSorting] = useState(seedStates.sorting);
51
53
  const [columnOrder, setColumnOrder] = useState(seedStates.columnOrder);
@@ -11,6 +11,8 @@ export interface SidebarService {
11
11
  }
12
12
  export type SidebarTarget = {
13
13
  kind: 'overview';
14
+ } | {
15
+ kind: 'people';
14
16
  } | {
15
17
  kind: 'management';
16
18
  } | {
@@ -20,12 +22,13 @@ export type SidebarTarget = {
20
22
  serviceId: string;
21
23
  };
22
24
  export type SidebarActive = SidebarTarget;
23
- export declare const Sidebar: ({ services, active, onSelect, onNavigate, onOpenSettings, showManagement, showDeveloper }: {
25
+ export declare const Sidebar: ({ services, active, onSelect, onNavigate, onOpenSettings, showManagement, showPeople, showDeveloper }: {
24
26
  services: SidebarService[];
25
27
  active: SidebarActive;
26
28
  onSelect: (target: SidebarTarget) => void;
27
29
  onNavigate?: () => void;
28
30
  onOpenSettings?: () => void;
29
31
  showManagement?: boolean;
32
+ showPeople?: boolean;
30
33
  showDeveloper?: boolean;
31
34
  }) => import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Database, LayoutGrid, Search, Settings, Wrench } from 'lucide-react';
2
+ import { Database, LayoutGrid, Search, Settings, Users, Wrench } from 'lucide-react';
3
3
  import { useState } from 'react';
4
4
  import { useLabels } from '../../i18n/index.js';
5
5
  import { cn, Input, ServiceIcon } from '../../primitives.js';
@@ -10,7 +10,7 @@ const rowClass = (active) => cn('flex w-full cursor-pointer items-center gap-2 r
10
10
  // The fixed left rail: Management link, then a searchable service list with Overview pinned first. Each
11
11
  // service shows its brand-color icon and a connection-confidence dot (when it has data). Pure +
12
12
  // prop-driven; the host owns navigation. `onNavigate` lets a mobile sheet close itself after a pick.
13
- export const Sidebar = ({ services, active, onSelect, onNavigate, onOpenSettings, showManagement = true, showDeveloper = true }) => {
13
+ export const Sidebar = ({ services, active, onSelect, onNavigate, onOpenSettings, showManagement = true, showPeople = false, showDeveloper = true }) => {
14
14
  const t = useLabels();
15
15
  const [query, setQuery] = useState('');
16
16
  const filtered = services.filter((s) => s.name.toLowerCase().includes(query.trim().toLowerCase()));
@@ -24,7 +24,7 @@ export const Sidebar = ({ services, active, onSelect, onNavigate, onOpenSettings
24
24
  return (_jsxs("div", { title: t.sidebarDisabledHint, "aria-disabled": true, className: cn(rowClass(false), 'cursor-not-allowed opacity-40 hover:bg-transparent'), children: [_jsx(ServiceIcon, { id: s.id, icon: s.icon, name: s.name, color: s.color, size: 16 }), _jsx("span", { className: "truncate", children: s.name })] }, s.id));
25
25
  }
26
26
  return (_jsxs("button", { className: rowClass(isActive), onClick: () => go({ kind: 'service', serviceId: s.id }), children: [_jsx(ServiceIcon, { id: s.id, icon: s.icon, name: s.name, color: s.color, size: 16 }), _jsx("span", { className: "truncate", children: s.name }), s.dot ? (_jsx(ConnDot, { state: s.dot, testing: s.testing, className: "ml-auto size-1.5 shrink-0", title: s.dot })) : null] }, s.id));
27
- }))] }), showManagement || showDeveloper || onOpenSettings ? (_jsxs("div", { className: "space-y-0.5 border-t pt-2", children: [_jsx("div", { className: "text-muted-foreground px-2 pb-1 text-[10px] font-medium tracking-wider uppercase", children: t.systemHeading }), showManagement ? (_jsxs("button", { className: rowClass(active.kind === 'management'), onClick: () => go({ kind: 'management' }), children: [_jsx(Database, { className: "size-4 shrink-0" }), t.navManagement] })) : null, showDeveloper ? (_jsxs("button", { className: rowClass(active.kind === 'developer'), onClick: () => go({ kind: 'developer' }), children: [_jsx(Wrench, { className: "size-4 shrink-0" }), t.navDeveloper] })) : null, onOpenSettings ? (_jsxs("button", { className: rowClass(false), onClick: () => {
27
+ }))] }), showManagement || showPeople || showDeveloper || onOpenSettings ? (_jsxs("div", { className: "space-y-0.5 border-t pt-2", children: [_jsx("div", { className: "text-muted-foreground px-2 pb-1 text-[10px] font-medium tracking-wider uppercase", children: t.systemHeading }), showManagement ? (_jsxs("button", { className: rowClass(active.kind === 'management'), onClick: () => go({ kind: 'management' }), children: [_jsx(Database, { className: "size-4 shrink-0" }), t.navManagement] })) : null, showPeople ? (_jsxs("button", { className: rowClass(active.kind === 'people'), onClick: () => go({ kind: 'people' }), children: [_jsx(Users, { className: "size-4 shrink-0" }), t.navPeople] })) : null, showDeveloper ? (_jsxs("button", { className: rowClass(active.kind === 'developer'), onClick: () => go({ kind: 'developer' }), children: [_jsx(Wrench, { className: "size-4 shrink-0" }), t.navDeveloper] })) : null, onOpenSettings ? (_jsxs("button", { className: rowClass(false), onClick: () => {
28
28
  onOpenSettings();
29
29
  onNavigate?.();
30
30
  }, children: [_jsx(Settings, { className: "size-4 shrink-0" }), t.navSettings] })) : null] })) : null] }));
@@ -11,6 +11,9 @@ export interface ButinLabels {
11
11
  navManagement: string;
12
12
  navOverview: string;
13
13
  navDeveloper: string;
14
+ navPeople: string;
15
+ peopleSummary: (people: number, services: number) => string;
16
+ peopleEmpty: string;
14
17
  logsHeading: string;
15
18
  navSettings: string;
16
19
  devNavBrowser: string;
@@ -171,6 +174,7 @@ export interface ButinLabels {
171
174
  refreshedAgo: (relative: string, absolute: string) => string;
172
175
  refreshWarning: string;
173
176
  connectPrompt: (service: string) => string;
177
+ disconnectedWithData: (service: string) => string;
174
178
  noDataYet: string;
175
179
  serviceCount: (n: number) => string;
176
180
  overviewSpending: string;
@@ -9,6 +9,9 @@ export const en = {
9
9
  navManagement: 'Management',
10
10
  navOverview: 'Overview',
11
11
  navDeveloper: 'Developer',
12
+ navPeople: 'People',
13
+ peopleSummary: (people, services) => `${people} ${people === 1 ? 'person' : 'people'} across ${services} ${services === 1 ? 'service' : 'services'}`,
14
+ peopleEmpty: 'No member data yet — services that report team members will show their people here after a refresh.',
12
15
  logsHeading: 'Logs',
13
16
  navSettings: 'Settings',
14
17
  devNavBrowser: 'Navigation browser',
@@ -179,6 +182,7 @@ export const en = {
179
182
  refreshedAgo: (relative, absolute) => `refreshed ${relative} · ${absolute}`,
180
183
  refreshWarning: 'Refresh failed — showing the last loaded data.',
181
184
  connectPrompt: (service) => `Connect ${service} to fetch its first report.`,
185
+ disconnectedWithData: (service) => `${service} is disconnected — showing the last loaded data.`,
182
186
  noDataYet: 'No data yet',
183
187
  serviceCount: (n) => `${n} ${n === 1 ? 'service' : 'services'}`,
184
188
  overviewSpending: 'Spending',
@@ -422,6 +426,9 @@ export const fr = {
422
426
  navManagement: 'Gestion',
423
427
  navOverview: 'Aperçu',
424
428
  navDeveloper: 'Développeur',
429
+ navPeople: 'Personnes',
430
+ peopleSummary: (people, services) => `${people} personne${people > 1 ? 's' : ''} sur ${services} service${services > 1 ? 's' : ''}`,
431
+ peopleEmpty: 'Aucune donnée de membres pour l’instant — les services qui rapportent des membres d’équipe apparaîtront ici après une actualisation.',
425
432
  logsHeading: 'Journaux',
426
433
  navSettings: 'Réglages',
427
434
  devNavBrowser: 'Navigateur',
@@ -592,6 +599,7 @@ export const fr = {
592
599
  refreshedAgo: (relative, absolute) => `actualisé ${relative} · ${absolute}`,
593
600
  refreshWarning: 'Échec du rafraîchissement — affichage des dernières données chargées.',
594
601
  connectPrompt: (service) => `Connectez ${service} pour récupérer ses premières données.`,
602
+ disconnectedWithData: (service) => `${service} est déconnecté — affichage des dernières données chargées.`,
595
603
  noDataYet: 'Aucune donnée',
596
604
  serviceCount: (n) => `${n} ${n === 1 ? 'service' : 'services'}`,
597
605
  overviewSpending: 'Dépenses',
@@ -20,8 +20,9 @@ const readToken = (el, name, fallback) => {
20
20
  // An unresolved var computes to the inherited color or fully-transparent black; treat that as "missing".
21
21
  return rgb && rgb !== 'rgba(0, 0, 0, 0)' ? rgb : fallback;
22
22
  };
23
- // Element whose computed tokens reflect the theme: the viewer root when embedded, else <html>.
24
- const probeElement = () => document.querySelector('.butin-viewer') ?? document.documentElement;
23
+ // Element whose computed tokens reflect the theme: the nearest `.butin` scope (an embed wrapper, or the app's
24
+ // <html>), which is where the token values are defined. Falls back to <html> if the class isn't found.
25
+ const probeElement = () => document.querySelector('.butin') ?? document.documentElement;
25
26
  const computeTheme = () => {
26
27
  const el = probeElement();
27
28
  const muted = readToken(el, '--muted-foreground', '#888');
package/dist/theme.css CHANGED
@@ -10,12 +10,21 @@
10
10
  build consumes — @butinapp/ui still ships no compiled CSS (no preflight, no utilities), so there is no
11
11
  second build to collide with. Importing the same tokens twice is idempotent.
12
12
 
13
+ SCOPED, not global: the concrete token VALUES live on the `.butin` scope class, never on the document
14
+ `:root`. That's the containment guarantee — a host embedding a Butin surface imports this file and its
15
+ own `:root` (and its own shadcn `--background` / `--chart-*` tokens) is never overwritten; Butin's values
16
+ apply only inside the `.butin` subtree. Every Butin root carries the class: the app's <html>, and each
17
+ embed wrapper (@butinapp/viewer's ButinViewer/ButinView). The @theme mapping stays global (it only
18
+ defines `--color-*` utility aliases as `var(--token)`, late-bound to whichever scope resolves them), so
19
+ Butin's `bg-background` inside `.butin` reads Butin's value and the host's utilities outside read the
20
+ host's.
21
+
13
22
  Theme-portable by design: an embedder that wants its OWN palette skips this import and defines the same
14
- semantic tokens itself; the components only ever reference the names. */
23
+ shadcn-named tokens on `.butin` itself; the components only ever reference the names. */
15
24
 
16
25
  @custom-variant dark (&:is(.dark *));
17
26
 
18
- :root {
27
+ .butin {
19
28
  color-scheme: light;
20
29
  --radius: 0.5rem;
21
30
  /* Surface elevation mirrors dark's stack (canvas → chrome → card), inverted: the canvas is a soft
@@ -54,7 +63,11 @@
54
63
  --sidebar-ring: oklch(0.58 0.09 188);
55
64
  }
56
65
 
57
- .dark {
66
+ /* Dark applies when the scope element itself is dark (`.butin.dark`, the standalone viewer's own toggle) or
67
+ sits under a dark ancestor (`.dark .butin`, a host that owns the theme). Both resolve the dark values only
68
+ within the Butin subtree. */
69
+ .butin.dark,
70
+ .dark .butin {
58
71
  color-scheme: dark;
59
72
  --background: oklch(0.177 0.011 261);
60
73
  --foreground: oklch(0.957 0.006 256);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butinapp/ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Butin's embeddable, theme-portable design system + the generic data-view renderer (DashboardRenderer, Overview, app shell). Pure, prop-driven, no Electron. Ships theme.css; a Tailwind host generates the utilities.",
5
5
  "homepage": "https://butin.app",
6
6
  "bugs": "https://github.com/butinapp/butin/issues",
@@ -52,8 +52,8 @@
52
52
  "luxon": "^3.7.2",
53
53
  "radix-ui": "^1.5.0",
54
54
  "tailwind-merge": "^3.6.0",
55
- "@butinapp/shapes": "0.1.0",
56
- "@butinapp/sdk": "0.1.0"
55
+ "@butinapp/sdk": "0.1.1",
56
+ "@butinapp/shapes": "0.1.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@testing-library/jest-dom": "^6.9.1",