@tradalab/lyra 3.2.0 → 3.4.0

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.
package/README.md CHANGED
@@ -26,8 +26,9 @@ The stylesheet pulls in Tailwind v4, so your app needs `@tailwindcss/postcss`.
26
26
  |---|---|---|
27
27
  | `@tradalab/lyra/utils` | `cn` | server-safe |
28
28
  | `@tradalab/lyra/hooks` | `useMobile` | client |
29
- | `@tradalab/lyra/ui` | 60 shadcn primitives + `AccountMenu`, `ActionButton`, `Container` | client |
30
- | `@tradalab/lyra/shell` | `AppShell`, `NavItem`, `NavGroup`, `Breadcrumbs`, `useActiveRoute` | client |
29
+ | `@tradalab/lyra/ui` | 60 shadcn primitives + `AccountMenu`, `ActionButton`, `Container`, `EmptyState`, `StatusBadge` | client |
30
+ | `@tradalab/lyra/blocks` | `Form`/`FormField` + RHF glue, `Panel`, `Confirm`/`useConfirm`, `Tree`, `TzPicker`, `OptionPicker`, `McpSettings`, `LlmSettings`, `PlatePicker`, `usePlate`, `useCompact`, `PreferenceScript` | client |
31
+ | `@tradalab/lyra/shell` | `AppShell`, `PageShell`, `PageHeader`, `NavItem`, `NavGroup`, `Breadcrumbs`, `useActiveRoute` | client |
31
32
  | `@tradalab/lyra/styles.css` | Tailwind v4 base + `radix-lyra` tokens (light/dark) | CSS |
32
33
 
33
34
  Client subpaths carry a build-injected `"use client"`; import them inside a
@@ -61,6 +62,37 @@ Infinity`). Position belongs to the viewport this package styles, so sonner's
61
62
  `position` and `richColors` props have no equivalent. Mount `<Toaster/>` once —
62
63
  apps on `@tradalab/nekit` already get it from `NekitProvider`.
63
64
 
65
+ ## Plate and density
66
+
67
+ `3.4.0` added a palette the reader picks. The colours live in `styles.css` behind
68
+ `[data-plate="<name>"]`, so an app only wires the choice:
69
+
70
+ ```tsx
71
+ import { PlatePicker, PreferenceScript, useCompact, usePlate } from "@tradalab/lyra/blocks"
72
+
73
+ // In the root layout, as early in the tree as possible.
74
+ <PreferenceScript />
75
+
76
+ // On a settings screen.
77
+ const [plate, setPlate] = usePlate()
78
+ <PlatePicker value={plate} onChange={setPlate} label="Accent colour" resetLabel="Default" />
79
+ ```
80
+
81
+ `usePlate` and `useCompact` remember the choice per DEVICE, in `localStorage`
82
+ under `lyra.plate` and `lyra.compact`, which is the same granularity as the
83
+ theme. Absence means the default, so turning one off removes its key rather than
84
+ storing "off".
85
+
86
+ Keep `suppressHydrationWarning` on `<html>`. The script writes attributes the
87
+ server did not render, which is the same reason `next-themes` needs it; every
88
+ tradalab layout and `template-web` already carry it.
89
+
90
+ Leaving out `<PreferenceScript />` is neither a compile error nor a runtime one.
91
+ The attributes then arrive from an effect, so the first paint is always the
92
+ neutral default and every reader who chose a colour watches it flip. It has to be
93
+ a raw inline script to beat that paint, which is why lyra renders it instead of
94
+ leaving it to each app.
95
+
64
96
  ## Develop
65
97
 
66
98
  ```bash
@@ -414,4 +414,71 @@ declare function LlmSettings({ api, labels, className, }: {
414
414
  className?: string;
415
415
  }): React.JSX.Element;
416
416
 
417
- export { ConfirmContext, ConfirmDialog, type ConfirmFn, type ConfirmOptions, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, type LlmCapability, type LlmDriver, type LlmField, type LlmFound, type LlmModel, type LlmProvider, type LlmProviderInput, type LlmPullProgress, LlmSettings, type LlmSettingsApi, type LlmSettingsLabels, type LlmSlot, type LlmState, type McpCall, type McpClient, McpSettings, type McpSettingsApi, type McpSettingsLabels, type McpStatus, type McpTool, OptionPicker, type OptionPickerLabels, Panel, PanelCard, type PanelCardProps, type PanelItemProps, type PickOption, TreeExpander, type TreeExpanderProps, TreeIcon, type TreeIconProps, TreeLabel, type TreeLabelProps, TreeLines, TreeNode, TreeNodeContent, type TreeNodeContentProps, type TreeNodeProps, TreeNodeTrigger, type TreeNodeTriggerProps, TreeProvider, type TreeProviderProps, TreeView, type TreeViewProps, TzPicker, type TzPickerLabels, type TzPickerProps, useConfirm, useFormField };
417
+ /**
418
+ * Plates: the palette a reader picks, applied by putting `data-plate` on any element.
419
+ *
420
+ * This module holds NAMES and nothing else. The colours live once, in `styles/globals.css`, and a
421
+ * swatch reads them the same way the rest of the UI does - by sitting inside a `data-plate` scope
422
+ * and using `bg-primary`. Copying the values here would be the second table that drifts.
423
+ *
424
+ * Absent `data-plate`, `--primary` is lyra's neutral. That is the default every app ships with, so
425
+ * adopting plates costs an app nothing until a reader chooses.
426
+ */
427
+ declare const PLATES: readonly ["pink", "rose", "crimson", "rust", "orange", "ochre", "amber", "honey", "gold", "olive", "lime", "green", "teal", "cyan", "azure", "blue", "indigo", "violet", "magenta"];
428
+ type Plate = (typeof PLATES)[number];
429
+ /**
430
+ * The plates certified to be told apart from each other, not merely from the status tokens.
431
+ *
432
+ * Only these may carry MEANING. The distinction is not decorative: a plate a reader picks for
433
+ * pleasure needs no relation to anyone else's, but a plate that says "which space you are typing
434
+ * into" stops working the moment two of them look alike - so that use gets the subset measured at
435
+ * dE76 >= 29.7 pairwise, in both themes.
436
+ */
437
+ declare const BEARING_PLATES: readonly ["pink", "rust", "amber", "olive", "green", "teal", "azure", "blue", "violet"];
438
+ type BearingPlate = (typeof BEARING_PLATES)[number];
439
+ declare function isPlate(s: string): s is Plate;
440
+ /**
441
+ * The plate for an id, from the certified subset.
442
+ *
443
+ * Derived, never chosen, and that is the point when the colour is a fence rather than a preference:
444
+ * if two organisations could both pick blue the signal stops working for both of them. Deriving on
445
+ * the CLIENT is deliberate too - the alternative is every server that shows a coloured surface
446
+ * keeping its own copy of the name list, which is the shape chat ended up in (a Go slice, a CSS
447
+ * block and a TS union, with nothing holding them together).
448
+ *
449
+ * FNV-1a rather than a cryptographic hash: this picks a colour, so spreading matters and secrecy
450
+ * does not, and it stays synchronous - SubtleCrypto would make every call site async for nothing.
451
+ */
452
+ declare function derivePlate(id: string): BearingPlate;
453
+
454
+ interface PlatePickerProps {
455
+ value: Plate | null;
456
+ onChange: (plate: Plate | null) => void;
457
+ /** Accessible name for the group, e.g. "Accent colour". */
458
+ label: string;
459
+ /** Accessible name for the neutral option, e.g. "Default". */
460
+ resetLabel: string;
461
+ /**
462
+ * Translated name per plate. Falls back to the plate's own key, which is English - fine for a
463
+ * developer tool, wrong for a product, and the reason this is a prop rather than a table in here.
464
+ */
465
+ plateLabels?: Partial<Record<Plate, string>>;
466
+ className?: string;
467
+ }
468
+ declare function PlatePicker({ value, onChange, label, resetLabel, plateLabels, className }: PlatePickerProps): React.JSX.Element;
469
+
470
+ /**
471
+ * The plate this device is set to, and a way to change it.
472
+ *
473
+ * A stored name that is no longer a plate - a rename, a downgrade to an older lyra - reads as no
474
+ * plate. The alternative is an attribute no rule matches, which looks identical to the neutral
475
+ * default on screen while the picker shows nothing selected at all, so the reader cannot tell that
476
+ * their choice was dropped or get back to a known state.
477
+ */
478
+ declare function usePlate(): [Plate | null, (plate: Plate | null) => void];
479
+
480
+ declare function useCompact(): [boolean, (compact: boolean) => void];
481
+
482
+ declare function PreferenceScript(): React.JSX.Element;
483
+
484
+ export { BEARING_PLATES, type BearingPlate, ConfirmContext, ConfirmDialog, type ConfirmFn, type ConfirmOptions, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, type LlmCapability, type LlmDriver, type LlmField, type LlmFound, type LlmModel, type LlmProvider, type LlmProviderInput, type LlmPullProgress, LlmSettings, type LlmSettingsApi, type LlmSettingsLabels, type LlmSlot, type LlmState, type McpCall, type McpClient, McpSettings, type McpSettingsApi, type McpSettingsLabels, type McpStatus, type McpTool, OptionPicker, type OptionPickerLabels, PLATES, Panel, PanelCard, type PanelCardProps, type PanelItemProps, type PickOption, type Plate, PlatePicker, type PlatePickerProps, PreferenceScript, TreeExpander, type TreeExpanderProps, TreeIcon, type TreeIconProps, TreeLabel, type TreeLabelProps, TreeLines, TreeNode, TreeNodeContent, type TreeNodeContentProps, type TreeNodeProps, TreeNodeTrigger, type TreeNodeTriggerProps, TreeProvider, type TreeProviderProps, TreeView, type TreeViewProps, TzPicker, type TzPickerLabels, type TzPickerProps, derivePlate, isPlate, useCompact, useConfirm, useFormField, usePlate };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { Label, Combobox, ComboboxInput, ComboboxContent, ComboboxEmpty, ComboboxList, ComboboxItem, Spinner, Alert, AlertDescription, FieldGroup, FieldSet, FieldLegend, FieldDescription, Field, FieldContent, FieldLabel, Badge, Switch, FieldSeparator, Progress } from '../chunk-DUWK53B5.js';
2
+ import { Label, Combobox, ComboboxInput, ComboboxContent, ComboboxEmpty, ComboboxList, ComboboxItem, Spinner, Alert, AlertDescription, FieldGroup, FieldSet, FieldLegend, FieldDescription, Field, FieldContent, FieldLabel, Badge, Switch, FieldSeparator, Progress, RadioGroup } from '../chunk-EJP7F42V.js';
3
3
  import { NativeSelect, NativeSelectOption } from '../chunk-FHM6VOPQ.js';
4
4
  import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../chunk-XJJQAQWJ.js';
5
5
  import { Dialog, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from '../chunk-XWGYXLIA.js';
@@ -7,8 +7,8 @@ import '../chunk-2HDU26FC.js';
7
7
  import { Button, Input } from '../chunk-VGOGWZCF.js';
8
8
  import { cn } from '../chunk-XH55BHUU.js';
9
9
  import * as React5 from 'react';
10
- import { createContext, useMemo, useState, useEffect, useContext, useCallback, useId } from 'react';
11
- import { Slot } from 'radix-ui';
10
+ import { createContext, useMemo, useState, useEffect, useContext, useCallback, useId, useSyncExternalStore } from 'react';
11
+ import { Slot, RadioGroup as RadioGroup$1 } from 'radix-ui';
12
12
  import { FormProvider, Controller, useFormContext } from 'react-hook-form';
13
13
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
14
  import { useRouter, useSearchParams } from 'next/navigation';
@@ -1302,6 +1302,154 @@ function LlmSettings({
1302
1302
  );
1303
1303
  }
1304
1304
 
1305
- export { ConfirmContext, ConfirmDialog, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, LlmSettings, McpSettings, OptionPicker, Panel, PanelCard, TreeExpander, TreeIcon, TreeLabel, TreeLines, TreeNode, TreeNodeContent, TreeNodeTrigger, TreeProvider, TreeView, TzPicker, useConfirm, useFormField };
1305
+ // src/blocks/plate.ts
1306
+ var PLATES = [
1307
+ "pink",
1308
+ "rose",
1309
+ "crimson",
1310
+ "rust",
1311
+ "orange",
1312
+ "ochre",
1313
+ "amber",
1314
+ "honey",
1315
+ "gold",
1316
+ "olive",
1317
+ "lime",
1318
+ "green",
1319
+ "teal",
1320
+ "cyan",
1321
+ "azure",
1322
+ "blue",
1323
+ "indigo",
1324
+ "violet",
1325
+ "magenta"
1326
+ ];
1327
+ var BEARING_PLATES = [
1328
+ "pink",
1329
+ "rust",
1330
+ "amber",
1331
+ "olive",
1332
+ "green",
1333
+ "teal",
1334
+ "azure",
1335
+ "blue",
1336
+ "violet"
1337
+ ];
1338
+ function isPlate(s) {
1339
+ return PLATES.includes(s);
1340
+ }
1341
+ function derivePlate(id) {
1342
+ let h = 2166136261;
1343
+ for (let i = 0; i < id.length; i++) {
1344
+ h ^= id.charCodeAt(i);
1345
+ h = Math.imul(h, 16777619) >>> 0;
1346
+ }
1347
+ return BEARING_PLATES[h % BEARING_PLATES.length];
1348
+ }
1349
+ var NONE = "none";
1350
+ function PlatePicker({ value, onChange, label, resetLabel, plateLabels, className }) {
1351
+ const swatch = "size-7 shrink-0 rounded-none border outline-2 -outline-offset-[3px] outline-transparent focus-visible:ring-2 focus-visible:ring-ring";
1352
+ return /* @__PURE__ */ jsxs(
1353
+ RadioGroup,
1354
+ {
1355
+ "aria-label": label,
1356
+ value: value ?? NONE,
1357
+ onValueChange: (v) => onChange(isPlate(v) ? v : null),
1358
+ className: cn("flex w-auto flex-wrap gap-2", className),
1359
+ children: [
1360
+ /* @__PURE__ */ jsx(
1361
+ RadioGroup$1.Item,
1362
+ {
1363
+ value: NONE,
1364
+ "aria-label": resetLabel,
1365
+ title: resetLabel,
1366
+ className: cn(swatch, "bg-muted border-border data-checked:outline-foreground")
1367
+ }
1368
+ ),
1369
+ PLATES.map((p) => /* @__PURE__ */ jsx(
1370
+ RadioGroup$1.Item,
1371
+ {
1372
+ value: p,
1373
+ "aria-label": plateLabels?.[p] ?? p,
1374
+ title: plateLabels?.[p] ?? p,
1375
+ "data-plate": p,
1376
+ className: cn(swatch, "bg-primary border-transparent data-checked:outline-primary-foreground")
1377
+ },
1378
+ p
1379
+ ))
1380
+ ]
1381
+ }
1382
+ );
1383
+ }
1384
+ var PLATE_KEY = "lyra.plate";
1385
+ var COMPACT_KEY = "lyra.compact";
1386
+ var listeners = /* @__PURE__ */ new Map();
1387
+ function read(key) {
1388
+ try {
1389
+ return localStorage.getItem(key);
1390
+ } catch {
1391
+ return null;
1392
+ }
1393
+ }
1394
+ function useStoredSetting(key) {
1395
+ const subscribe = useCallback(
1396
+ (onChange) => {
1397
+ const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
1398
+ listeners.set(key, set);
1399
+ set.add(onChange);
1400
+ const onStorage = (e) => {
1401
+ if (e.key === key) onChange();
1402
+ };
1403
+ window.addEventListener("storage", onStorage);
1404
+ return () => {
1405
+ set.delete(onChange);
1406
+ window.removeEventListener("storage", onStorage);
1407
+ };
1408
+ },
1409
+ [key]
1410
+ );
1411
+ const value = useSyncExternalStore(
1412
+ subscribe,
1413
+ () => read(key),
1414
+ () => null
1415
+ );
1416
+ const store = useCallback((next) => {
1417
+ try {
1418
+ if (next === null) localStorage.removeItem(key);
1419
+ else localStorage.setItem(key, next);
1420
+ } catch {
1421
+ }
1422
+ listeners.get(key)?.forEach((l) => l());
1423
+ }, [key]);
1424
+ return [value, store];
1425
+ }
1426
+ function useRootAttribute(name, value) {
1427
+ useEffect(() => {
1428
+ const root = document.documentElement;
1429
+ if (value === null) root.removeAttribute(name);
1430
+ else root.setAttribute(name, value);
1431
+ }, [name, value]);
1432
+ }
1433
+
1434
+ // src/blocks/use-plate.ts
1435
+ function usePlate() {
1436
+ const [stored, store] = useStoredSetting(PLATE_KEY);
1437
+ const plate = stored !== null && isPlate(stored) ? stored : null;
1438
+ useRootAttribute("data-plate", plate);
1439
+ return [plate, store];
1440
+ }
1441
+ function useCompact() {
1442
+ const [stored, store] = useStoredSetting(COMPACT_KEY);
1443
+ const compact = stored === "true";
1444
+ const setCompact = useCallback((next) => store(next ? "true" : null), [store]);
1445
+ useRootAttribute("data-compact", compact ? "true" : null);
1446
+ return [compact, setCompact];
1447
+ }
1448
+ var SCRIPT = `try{var d=document.documentElement,p=localStorage.getItem("${PLATE_KEY}");if(p)d.setAttribute("data-plate",p);if(localStorage.getItem("${COMPACT_KEY}")==="true")d.setAttribute("data-compact","true")}catch(e){}`;
1449
+ function PreferenceScript() {
1450
+ return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: SCRIPT } });
1451
+ }
1452
+
1453
+ export { BEARING_PLATES, ConfirmContext, ConfirmDialog, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, LlmSettings, McpSettings, OptionPicker, PLATES, Panel, PanelCard, PlatePicker, PreferenceScript, TreeExpander, TreeIcon, TreeLabel, TreeLines, TreeNode, TreeNodeContent, TreeNodeTrigger, TreeProvider, TreeView, TzPicker, derivePlate, isPlate, useCompact, useConfirm, useFormField, usePlate };
1306
1454
  //# sourceMappingURL=index.js.map
1307
1455
  //# sourceMappingURL=index.js.map