@tradalab/lyra 2.1.0 → 2.3.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
@@ -1,99 +1,99 @@
1
- # @tradalab/lyra
2
-
3
- > Tradalab's UI kit — shadcn (`radix-lyra` style) primitives, composed
4
- > components, and an app shell for **React 19 / Next 16 / Tailwind v4**.
5
-
6
- ESM-only. Extracted from Tradalab's foundation kit so every company app shares
7
- the same component surface.
8
-
9
- ## Install
10
-
11
- ```bash
12
- pnpm add @tradalab/lyra
13
- # peers: react ^19, react-dom ^19 (next ^16 only if you use /shell)
14
- ```
15
-
16
- The stylesheet pulls in Tailwind v4, so your app needs `@tailwindcss/postcss`.
17
-
18
- ```css
19
- /* app/globals.css */
20
- @import "@tradalab/lyra/styles.css";
21
- ```
22
-
23
- ## Subpaths
24
-
25
- | Subpath | Exports | Boundary |
26
- |---|---|---|
27
- | `@tradalab/lyra/utils` | `cn` | server-safe |
28
- | `@tradalab/lyra/hooks` | `useMobile` | client |
29
- | `@tradalab/lyra/ui` | 60 shadcn primitives + `AccountMenu`, `Container` | client |
30
- | `@tradalab/lyra/shell` | `AppShell`, `NavItem`, `NavGroup`, `Breadcrumbs`, `useActiveRoute` | client |
31
- | `@tradalab/lyra/styles.css` | Tailwind v4 base + `radix-lyra` tokens (light/dark) | CSS |
32
-
33
- Client subpaths carry a build-injected `"use client"`; import them inside a
34
- client tree or pass via `children` — don't import them into a Server Component's
35
- own module.
36
-
37
- ```tsx
38
- import { Button, Card, CardHeader } from "@tradalab/lyra/ui"
39
- import { AppShell, NavItem } from "@tradalab/lyra/shell"
40
- ```
41
-
42
- ## Toast
43
-
44
- `2.0.0` replaced the sonner-based toast with shadcn's Base UI Toast. `sonner` is
45
- gone — it is no longer a peer dependency, and apps should drop it.
46
-
47
- `toast` is a **manager object, not a callable**:
48
-
49
- ```tsx
50
- import { Toaster, toast } from "@tradalab/lyra/ui"
51
-
52
- toast.add({ title: "Saved" })
53
- toast.add({ title: "Upload failed", description: err.message, type: "error" })
54
- const id = toast.add({ title: "Uploading…", type: "loading", timeout: 0 })
55
- toast.close(id)
56
- ```
57
-
58
- `type` is `"success" | "error" | "info" | "warning" | "loading"` — there is no
59
- `toast.success("x")`. `timeout: 0` never auto-dismisses (sonner's `duration:
60
- Infinity`). Position belongs to the viewport this package styles, so sonner's
61
- `position` and `richColors` props have no equivalent. Mount `<Toaster/>` once —
62
- apps on `@tradalab/nekit` already get it from `NekitProvider`.
63
-
64
- ## Develop
65
-
66
- ```bash
67
- make install # pnpm install
68
- make build # tsup + inject-use-client + verify-build-output
69
- make verify # typecheck + lint + test
70
- make ui-add <name> # scaffold a shadcn primitive, adapt, rebuild barrel
71
- make ui-update <name> # overwrite a primitive with the latest upstream version
72
- make ui-sync # re-adapt every primitive + rebuild barrel
73
- make ui-diff # report which primitives changed upstream (exit 1 if any)
74
- ```
75
-
76
- **Staying current with shadcn** — `make ui-diff` compares every primitive
77
- against the live `radix-lyra` registry and exits non-zero when one has moved.
78
- It normalises away everything the CLI would rewrite anyway (icon-library
79
- placeholders, `cn-*` template classes, import aliases, prettier line-wrapping),
80
- so what it prints is real change. Prefer it over `shadcn add --diff`, which
81
- caps its output at five files per run.
82
-
83
- A primitive lyra deliberately diverges from carries a `customised` note in
84
- `primitives.lock.json`; `ui-diff` lists those separately instead of failing on
85
- them forever. Document the divergence in a JSDoc block in the file too —
86
- `make ui-update` will overwrite it, and that note is what tells the next person
87
- to put it back.
88
-
89
- **Layout** — `src/<layer>/` with a barrel `index.ts`; one tsup entry per
90
- subpath; ESM-only to `dist/`. **Layers** (enforced by `eslint-plugin-boundaries`):
91
- `utils`/`hooks` (foundation) → `ui` → `shell`. No upward imports.
92
-
93
- **`"use client"` is not in source** — it is injected onto the client entry
94
- artifacts post-build by `scripts/inject-use-client.mjs`, then asserted by
95
- `scripts/verify-build-output.mjs`. `scripts/sync-ui.mjs` adapts shadcn-CLI
96
- output to repo conventions.
97
-
98
- **Release** — `pnpm publish --no-git-checks` (the `prepublishOnly` hook builds
99
- first). Published to the npm public registry.
1
+ # @tradalab/lyra
2
+
3
+ > Tradalab's UI kit — shadcn (`radix-lyra` style) primitives, composed
4
+ > components, and an app shell for **React 19 / Next 16 / Tailwind v4**.
5
+
6
+ ESM-only. Extracted from Tradalab's foundation kit so every company app shares
7
+ the same component surface.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @tradalab/lyra
13
+ # peers: react ^19, react-dom ^19 (next ^16 only if you use /shell)
14
+ ```
15
+
16
+ The stylesheet pulls in Tailwind v4, so your app needs `@tailwindcss/postcss`.
17
+
18
+ ```css
19
+ /* app/globals.css */
20
+ @import "@tradalab/lyra/styles.css";
21
+ ```
22
+
23
+ ## Subpaths
24
+
25
+ | Subpath | Exports | Boundary |
26
+ |---|---|---|
27
+ | `@tradalab/lyra/utils` | `cn` | server-safe |
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 |
31
+ | `@tradalab/lyra/styles.css` | Tailwind v4 base + `radix-lyra` tokens (light/dark) | CSS |
32
+
33
+ Client subpaths carry a build-injected `"use client"`; import them inside a
34
+ client tree or pass via `children` — don't import them into a Server Component's
35
+ own module.
36
+
37
+ ```tsx
38
+ import { Button, Card, CardHeader } from "@tradalab/lyra/ui"
39
+ import { AppShell, NavItem } from "@tradalab/lyra/shell"
40
+ ```
41
+
42
+ ## Toast
43
+
44
+ `2.0.0` replaced the sonner-based toast with shadcn's Base UI Toast. `sonner` is
45
+ gone — it is no longer a peer dependency, and apps should drop it.
46
+
47
+ `toast` is a **manager object, not a callable**:
48
+
49
+ ```tsx
50
+ import { Toaster, toast } from "@tradalab/lyra/ui"
51
+
52
+ toast.add({ title: "Saved" })
53
+ toast.add({ title: "Upload failed", description: err.message, type: "error" })
54
+ const id = toast.add({ title: "Uploading…", type: "loading", timeout: 0 })
55
+ toast.close(id)
56
+ ```
57
+
58
+ `type` is `"success" | "error" | "info" | "warning" | "loading"` — there is no
59
+ `toast.success("x")`. `timeout: 0` never auto-dismisses (sonner's `duration:
60
+ Infinity`). Position belongs to the viewport this package styles, so sonner's
61
+ `position` and `richColors` props have no equivalent. Mount `<Toaster/>` once —
62
+ apps on `@tradalab/nekit` already get it from `NekitProvider`.
63
+
64
+ ## Develop
65
+
66
+ ```bash
67
+ make install # pnpm install
68
+ make build # tsup + inject-use-client + verify-build-output
69
+ make verify # typecheck + lint + test
70
+ make ui-add <name> # scaffold a shadcn primitive, adapt, rebuild barrel
71
+ make ui-update <name> # overwrite a primitive with the latest upstream version
72
+ make ui-sync # re-adapt every primitive + rebuild barrel
73
+ make ui-diff # report which primitives changed upstream (exit 1 if any)
74
+ ```
75
+
76
+ **Staying current with shadcn** — `make ui-diff` compares every primitive
77
+ against the live `radix-lyra` registry and exits non-zero when one has moved.
78
+ It normalises away everything the CLI would rewrite anyway (icon-library
79
+ placeholders, `cn-*` template classes, import aliases, prettier line-wrapping),
80
+ so what it prints is real change. Prefer it over `shadcn add --diff`, which
81
+ caps its output at five files per run.
82
+
83
+ A primitive lyra deliberately diverges from carries a `customised` note in
84
+ `primitives.lock.json`; `ui-diff` lists those separately instead of failing on
85
+ them forever. Document the divergence in a JSDoc block in the file too —
86
+ `make ui-update` will overwrite it, and that note is what tells the next person
87
+ to put it back.
88
+
89
+ **Layout** — `src/<layer>/` with a barrel `index.ts`; one tsup entry per
90
+ subpath; ESM-only to `dist/`. **Layers** (enforced by `eslint-plugin-boundaries`):
91
+ `utils`/`hooks` (foundation) → `ui` → `shell`. No upward imports.
92
+
93
+ **`"use client"` is not in source** — it is injected onto the client entry
94
+ artifacts post-build by `scripts/inject-use-client.mjs`, then asserted by
95
+ `scripts/verify-build-output.mjs`. `scripts/sync-ui.mjs` adapts shadcn-CLI
96
+ output to repo conventions.
97
+
98
+ **Release** — `pnpm publish --no-git-checks` (the `prepublishOnly` hook builds
99
+ first). Published to the npm public registry.
@@ -125,4 +125,72 @@ declare const TreeIcon: ({ icon, hasChildren, className, ...props }: TreeIconPro
125
125
  type TreeLabelProps = HTMLAttributes<HTMLSpanElement>;
126
126
  declare const TreeLabel: ({ className, ...props }: TreeLabelProps) => React.JSX.Element;
127
127
 
128
- export { ConfirmContext, ConfirmDialog, type ConfirmFn, type ConfirmOptions, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Panel, PanelCard, type PanelCardProps, type PanelItemProps, 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, useConfirm, useFormField };
128
+ interface TzPickerLabels {
129
+ /** Shown when the value is empty and `allowInherit`. Default: "Inherit". */
130
+ inherit?: string;
131
+ /** Placeholder for the search input. Default: "Search timezone". */
132
+ search?: string;
133
+ /** Shown when the filter matches nothing. Default: "No match". */
134
+ noMatch?: string;
135
+ }
136
+ interface TzPickerProps {
137
+ /** IANA zone name, or "" for inherit. */
138
+ value: string;
139
+ onChange: (tz: string) => void;
140
+ /**
141
+ * Whether an empty value is meaningful here. It usually is: a device inherits from its site,
142
+ * a site from its unit, a unit from its parent. Set false where a zone must be chosen.
143
+ */
144
+ allowInherit?: boolean;
145
+ labels?: TzPickerLabels;
146
+ disabled?: boolean;
147
+ id?: string;
148
+ className?: string;
149
+ }
150
+ declare function TzPicker({ value, onChange, allowInherit, labels, disabled, id, className, }: TzPickerProps): React.JSX.Element;
151
+
152
+ /**
153
+ * Pick one id from a known set.
154
+ *
155
+ * The set is INJECTED. That is what lets this live in lyra: it never learns where the
156
+ * options came from, so it works the same for org members, groups, or anything else the app
157
+ * has already loaded - which is exactly how the three copies it replaces differed.
158
+ *
159
+ * Why it exists at all: the screens that use it used to ask for raw uuids typed into a text
160
+ * box. Nobody knows a uuid, so the only way anyone filled those fields was by copying from
161
+ * somewhere else, and a copy can be off by one row. In `people` that attached a personnel
162
+ * record to the wrong account - a wrong-but-valid uid passes every server check and is found
163
+ * by whoever opens someone else's file under their own name.
164
+ *
165
+ * Filtering is client-side, deliberately: every set here is org-scoped and already loaded for
166
+ * the surrounding screen, so matching locally is instant and costs no round trip. A picker
167
+ * paging over every account that exists (admin's platform-wide search) is a different
168
+ * component with a different failure mode - see `PeoplePicker` in `@tradalab/lyra/people`,
169
+ * which takes an async `search` and does not hold a controlled value.
170
+ */
171
+ /** One choosable thing: an id plus what a person actually recognises it by. */
172
+ interface PickOption {
173
+ id: string;
174
+ label: string;
175
+ /** Secondary line - an email, a status. Searchable along with the label. */
176
+ hint?: string;
177
+ }
178
+ interface OptionPickerLabels {
179
+ /** Shown when nothing is selected. */
180
+ placeholder?: string;
181
+ /** Placeholder inside the search box. */
182
+ search?: string;
183
+ /** Shown when the filter matches nothing. */
184
+ empty?: string;
185
+ }
186
+ declare function OptionPicker({ value, onChange, options, labels, disabled, id, className, }: {
187
+ value: string;
188
+ onChange: (id: string) => void;
189
+ options: PickOption[];
190
+ labels?: OptionPickerLabels;
191
+ disabled?: boolean;
192
+ id?: string;
193
+ className?: string;
194
+ }): React.JSX.Element;
195
+
196
+ export { ConfirmContext, ConfirmDialog, type ConfirmFn, type ConfirmOptions, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, 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 };
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { Label } from '../chunk-QGUKTRBR.js';
2
+ import { Label, Combobox, ComboboxInput, ComboboxContent, ComboboxEmpty, ComboboxList, ComboboxItem } from '../chunk-VA6K3TP3.js';
3
3
  import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../chunk-LZZ3UOPT.js';
4
- import { Dialog, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from '../chunk-APYQOJAJ.js';
5
- import { Button } from '../chunk-5Q4UTRYU.js';
4
+ import { Dialog, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from '../chunk-XWGYXLIA.js';
5
+ import '../chunk-2HDU26FC.js';
6
+ import { Button } from '../chunk-VGOGWZCF.js';
6
7
  import { cn } from '../chunk-XH55BHUU.js';
7
8
  import * as React from 'react';
8
9
  import { createContext, useMemo, useState, useEffect, useContext, useCallback, useId } from 'react';
@@ -417,7 +418,160 @@ var TreeIcon = ({ icon, hasChildren = false, className, ...props }) => {
417
418
  return /* @__PURE__ */ jsx("div", { className: cn("mr-2 flex h-4 w-4 items-center justify-center text-muted-foreground transition-transform", className), ...props, children: icon || getDefaultIcon() });
418
419
  };
419
420
  var TreeLabel = ({ className, ...props }) => /* @__PURE__ */ jsx("span", { className: cn("font flex-1 truncate text-sm", className), ...props });
421
+ var FALLBACK_ZONES = [
422
+ "UTC",
423
+ "Asia/Ho_Chi_Minh",
424
+ "Asia/Bangkok",
425
+ "Asia/Singapore",
426
+ "Asia/Tokyo",
427
+ "Asia/Seoul",
428
+ "Asia/Shanghai",
429
+ "Asia/Kolkata",
430
+ "Europe/London",
431
+ "Europe/Berlin",
432
+ "America/New_York",
433
+ "America/Los_Angeles"
434
+ ];
435
+ var cachedZones = null;
436
+ function allZones() {
437
+ if (cachedZones) return cachedZones;
438
+ let names;
439
+ try {
440
+ const supported = Intl.supportedValuesOf;
441
+ names = supported ? supported("timeZone") : FALLBACK_ZONES;
442
+ } catch {
443
+ names = FALLBACK_ZONES;
444
+ }
445
+ cachedZones = names.map((name) => ({ name, offset: offsetOf(name) }));
446
+ return cachedZones;
447
+ }
448
+ function offsetOf(tz) {
449
+ try {
450
+ const parts = new Intl.DateTimeFormat("en-US", {
451
+ timeZone: tz,
452
+ timeZoneName: "shortOffset"
453
+ }).formatToParts(/* @__PURE__ */ new Date());
454
+ return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
455
+ } catch {
456
+ return "";
457
+ }
458
+ }
459
+ function browserTz() {
460
+ try {
461
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
462
+ } catch {
463
+ return "UTC";
464
+ }
465
+ }
466
+ function TzPicker({
467
+ value,
468
+ onChange,
469
+ allowInherit = true,
470
+ labels,
471
+ disabled,
472
+ id,
473
+ className
474
+ }) {
475
+ const l = {
476
+ inherit: labels?.inherit ?? "Inherit",
477
+ search: labels?.search ?? "Search timezone",
478
+ noMatch: labels?.noMatch ?? "No match"
479
+ };
480
+ const own = React.useMemo(() => browserTz(), []);
481
+ const entries = React.useMemo(() => {
482
+ const zones = allZones();
483
+ const out = [];
484
+ if (allowInherit) out.push({ tz: "", name: l.inherit, offset: "" });
485
+ const ownZone = zones.find((z) => z.name === own);
486
+ if (ownZone) out.push({ tz: own, name: own, offset: ownZone.offset });
487
+ for (const z of zones) {
488
+ if (z.name === own) continue;
489
+ out.push({ tz: z.name, name: z.name, offset: z.offset });
490
+ }
491
+ if (value !== "" && !zones.some((z) => z.name === value)) {
492
+ out.push({ tz: value, name: value, offset: offsetOf(value) });
493
+ }
494
+ return out;
495
+ }, [allowInherit, l.inherit, own, value]);
496
+ const selected = entries.find((e) => e.tz === value) ?? entries[0];
497
+ return /* @__PURE__ */ jsxs(
498
+ Combobox,
499
+ {
500
+ items: entries,
501
+ value: selected,
502
+ onValueChange: (item) => onChange(item?.tz ?? ""),
503
+ itemToStringLabel: (e) => e.offset ? `${e.name} \xB7 ${e.offset}` : e.name,
504
+ itemToStringValue: (e) => e.tz,
505
+ isItemEqualToValue: (a, b) => a.tz === b.tz,
506
+ disabled,
507
+ children: [
508
+ /* @__PURE__ */ jsx(
509
+ ComboboxInput,
510
+ {
511
+ id,
512
+ placeholder: l.search,
513
+ className: className ?? "w-full",
514
+ showClear: allowInherit
515
+ }
516
+ ),
517
+ /* @__PURE__ */ jsxs(ComboboxContent, { children: [
518
+ /* @__PURE__ */ jsx(ComboboxEmpty, { children: l.noMatch }),
519
+ /* @__PURE__ */ jsx(ComboboxList, { children: (entry) => /* @__PURE__ */ jsxs(ComboboxItem, { value: entry, children: [
520
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: entry.name }),
521
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: entry.offset })
522
+ ] }, entry.tz || "__inherit") })
523
+ ] })
524
+ ]
525
+ }
526
+ );
527
+ }
528
+ var FALLBACK = {
529
+ placeholder: "Select\u2026",
530
+ search: "Search",
531
+ empty: "No match"
532
+ };
533
+ function OptionPicker({
534
+ value,
535
+ onChange,
536
+ options,
537
+ labels,
538
+ disabled,
539
+ id,
540
+ className
541
+ }) {
542
+ const t = (k) => labels?.[k] ?? FALLBACK[k];
543
+ const placeholder = labels?.placeholder ?? FALLBACK.placeholder;
544
+ const entries = React.useMemo(() => {
545
+ const out = [{ id: "", label: placeholder, none: true }, ...options];
546
+ if (value !== "" && !options.some((o) => o.id === value)) {
547
+ out.push({ id: value, label: value });
548
+ }
549
+ return out;
550
+ }, [options, value, placeholder]);
551
+ const selected = entries.find((e) => e.id === value) ?? entries[0];
552
+ return /* @__PURE__ */ jsxs(
553
+ Combobox,
554
+ {
555
+ items: entries,
556
+ value: selected,
557
+ onValueChange: (e) => onChange(e?.id ?? ""),
558
+ itemToStringLabel: (e) => e.none ? e.label : `${e.label} ${e.hint ?? ""} ${e.id}`,
559
+ isItemEqualToValue: (a, b) => a.id === b.id,
560
+ disabled,
561
+ children: [
562
+ /* @__PURE__ */ jsx(ComboboxInput, { id, placeholder: t("search"), className: className ?? "w-full" }),
563
+ /* @__PURE__ */ jsxs(ComboboxContent, { children: [
564
+ /* @__PURE__ */ jsx(ComboboxEmpty, { children: t("empty") }),
565
+ /* @__PURE__ */ jsx(ComboboxList, { children: (entry) => /* @__PURE__ */ jsx(ComboboxItem, { value: entry, children: /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
566
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm", children: entry.label }),
567
+ entry.hint ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate text-xs", children: entry.hint }) : null
568
+ ] }) }, entry.id || "__none") })
569
+ ] })
570
+ ]
571
+ }
572
+ );
573
+ }
420
574
 
421
- export { ConfirmContext, ConfirmDialog, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Panel, PanelCard, TreeExpander, TreeIcon, TreeLabel, TreeLines, TreeNode, TreeNodeContent, TreeNodeTrigger, TreeProvider, TreeView, useConfirm, useFormField };
575
+ export { ConfirmContext, ConfirmDialog, ConfirmProvider, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, OptionPicker, Panel, PanelCard, TreeExpander, TreeIcon, TreeLabel, TreeLines, TreeNode, TreeNodeContent, TreeNodeTrigger, TreeProvider, TreeView, TzPicker, useConfirm, useFormField };
422
576
  //# sourceMappingURL=index.js.map
423
577
  //# sourceMappingURL=index.js.map