@columenlabs/lumen-ui 0.1.172 → 0.1.174

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.
@@ -14,10 +14,13 @@ export declare function tintedIcon(icon: ComponentType<{
14
14
  className?: string;
15
15
  }>;
16
16
  /** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
17
- export declare function ColorChip({ color, icon: Icon, children, }: {
17
+ export declare function ColorChip({ color, icon: Icon, children, className, title, }: {
18
18
  color?: string;
19
19
  icon?: ComponentType<{
20
20
  className?: string;
21
21
  }>;
22
22
  children: ReactNode;
23
+ className?: string;
24
+ /** The full text on hover, for a chip that may end in an ellipsis. */
25
+ title?: string;
23
26
  }): import("react").JSX.Element;
package/dist/color-dot.js CHANGED
@@ -32,12 +32,12 @@ export function tintedIcon(icon, color) {
32
32
  return out;
33
33
  }
34
34
  /** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
35
- export function ColorChip({ color, icon: Icon, children, }) {
36
- return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color
35
+ export function ColorChip({ color, icon: Icon, children, className, title, }) {
36
+ return (_jsxs("span", { title: title, className: cn('inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground', className), style: color
37
37
  ? {
38
38
  background: `color-mix(in oklab, ${color} 18%, transparent)`,
39
39
  // The colour pulled toward the text colour, so a light hue (yellow, sky) still reads.
40
40
  color: `color-mix(in oklab, ${color} 55%, var(--foreground))`,
41
41
  }
42
- : undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), children] }));
42
+ : undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: children })] }));
43
43
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { LumenLayout, type LumenLayoutCard, type LumenLayoutProps, type LumenLay
3
3
  export { LumenViewCard, type LumenViewCardProps } from './lumen-view-card.js';
4
4
  export { LumenSelector, LumenSelectorList, type LumenSelectorListHandle, type LumenSelectorProps, type LumenOption, type LumenOptionGroup, SelectorRowContent, } from './lumen-selector.js';
5
5
  export { ColorChip, ColorDot, colorIcon } from './color-dot.js';
6
+ export { TagChips, tagColor, tagIcon } from './tag-chips.js';
6
7
  export { EditCell, EditInputCell } from './view-edit-cell.js';
7
8
  export { type CardOnScreen, cardsOnScreen, kindName, onScreen, pointAt, pointAtCard, type PointedAt, type SectionOnScreen, sectionsOnScreen, spotlight, targetEl, textOf, } from './spotlight.js';
8
9
  export { LinkDot, LinkPicker, type LinkColor, linkVar } from './lumen-links.js';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { LumenLayout, domainKeys, followRows, } from './lumen-layout.js';
10
10
  export { LumenViewCard } from './lumen-view-card.js';
11
11
  export { LumenSelector, LumenSelectorList, SelectorRowContent, } from './lumen-selector.js';
12
12
  export { ColorChip, ColorDot, colorIcon } from './color-dot.js';
13
+ export { TagChips, tagColor, tagIcon } from './tag-chips.js';
13
14
  export { EditCell, EditInputCell } from './view-edit-cell.js';
14
15
  export { cardsOnScreen, kindName, onScreen, pointAt, pointAtCard, sectionsOnScreen, spotlight, targetEl, textOf, } from './spotlight.js';
15
16
  export { LinkDot, LinkPicker, linkVar } from './lumen-links.js';
@@ -29,6 +29,7 @@ import { MAX_COMPARE, useSymbolChart } from './market/lumen-chart.js';
29
29
  import { LumenPageFooter, LumenPager } from './pager.js';
30
30
  import { maskAmount, useAmountsHidden } from './shell.js';
31
31
  import { useSymbolSheet } from './symbol-sheet-context.js';
32
+ import { TagChips, tagColor, tagIcon } from './tag-chips.js';
32
33
  import { Badge } from './ui/badge.js';
33
34
  import { Button } from './ui/button.js';
34
35
  import { Checkbox } from './ui/checkbox.js';
@@ -66,6 +67,17 @@ export function mergeRows(domain, rows, groupBy) {
66
67
  const MINUS = '−';
67
68
  /** Text cells longer than this many characters are cut to one line with a tooltip. */
68
69
  const LONG_TEXT = 48;
70
+ /** A tags field's tags for a row: its `values`, else its text split on commas. */
71
+ function tagsOf(f, row, v) {
72
+ if (f.values)
73
+ return f.values(row);
74
+ return typeof v === 'string'
75
+ ? v
76
+ .split(',')
77
+ .map((t) => t.trim())
78
+ .filter(Boolean)
79
+ : [];
80
+ }
69
81
  /** Rows of a view the assistant sees with each message. */
70
82
  const ASSISTANT_ROWS = 10;
71
83
  const formatter = (digits) => new Intl.NumberFormat('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
@@ -555,12 +567,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
555
567
  const valueCellOf = (f, row, chip, v) => {
556
568
  if (f.render)
557
569
  return f.render(row);
570
+ if (f.tags && !f.edit?.many)
571
+ return (_jsx(TagChips, { tags: tagsOf(f, row, v), colorOf: f.color, iconOf: f.iconOf, empty: f.empty }));
558
572
  if (!f.edit)
559
573
  return (_jsx(ViewValueCell, { value: v, format: f.format, chip: chip, tone: chip ? f.tone?.(row) : undefined, color: typeof v === 'string' ? f.color?.(v) : undefined, amount: f.amount, compact: f.compact, empty: f.empty }));
560
574
  const edit = f.edit;
561
575
  const many = edit.many;
562
576
  if (many)
563
- return (_jsx(EditManyCell, { label: f.label, values: f.values?.(row) ?? (typeof v === 'string' && v !== '' ? [v] : []), options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, note: edit.note?.(row), colorOf: f.color, iconOf: f.iconOf, onSave: (values) => many(row, values) }));
577
+ return (_jsx(EditManyCell, { label: f.label, values: f.tags
578
+ ? tagsOf(f, row, v)
579
+ : (f.values?.(row) ?? (typeof v === 'string' && v !== '' ? [v] : [])), options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, note: edit.note?.(row), colorOf: f.tags ? (t) => f.color?.(t) ?? tagColor(t) : f.color, iconOf: f.tags ? (t) => f.iconOf?.(t) ?? tagIcon(t) : f.iconOf, onSave: (values) => many(row, values) }));
564
580
  const save = edit.save;
565
581
  if (!save)
566
582
  return _jsx(ViewValueCell, { value: v, format: f.format, chip: chip });
@@ -895,8 +911,10 @@ function PivotTable({ domain, rows, pivot, }) {
895
911
  /** A group heading's mark: its value's icon in its colour, else its colour's dot, else the
896
912
  * field's icon. */
897
913
  function GroupMark({ field, value }) {
898
- const color = field.color?.(value);
899
- const Icon = field.iconOf?.(value);
914
+ // A tags field's one tag takes its own colour and icon, as its chips do.
915
+ const tag = field.tags && !value.includes(',');
916
+ const color = field.color?.(value) ?? (tag ? tagColor(value) : undefined);
917
+ const Icon = field.iconOf?.(value) ?? (tag ? tagIcon(value) : undefined);
900
918
  if (Icon)
901
919
  return (_jsx("span", { className: "inline-flex", style: color ? { color } : undefined, children: _jsx(Icon, { className: "size-3.5" }) }));
902
920
  if (color)
@@ -0,0 +1,22 @@
1
+ import type { ComponentType } from 'react';
2
+ type Icon = ComponentType<{
3
+ className?: string;
4
+ }>;
5
+ /** A tag's colour, picked from its name: the same tag is the same colour everywhere. */
6
+ export declare function tagColor(tag: string): string;
7
+ /** A tag's icon from its words (`cybersecurity` a shield, `ai` sparkles), or none when no word fits. */
8
+ export declare function tagIcon(tag: string): Icon | undefined;
9
+ /**
10
+ * A row's tags as chips on one line: each in its colour with its icon. Past `max` the rest fold
11
+ * into a `+N` chip whose hover names them all, so a table row never wraps. `colorOf` and `iconOf`
12
+ * override the name-picked colour and icon (an app's own label colours).
13
+ */
14
+ export declare function TagChips({ tags, max, colorOf, iconOf, empty, }: {
15
+ tags: readonly string[];
16
+ max?: number;
17
+ colorOf?: (tag: string) => string | undefined;
18
+ iconOf?: (tag: string) => Icon | undefined;
19
+ /** What no tags says; left out: a dash. */
20
+ empty?: string;
21
+ }): import("react").JSX.Element;
22
+ export {};
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Tags (a row's several free-form labels: `ai`, `cybersecurity`, `favorite`) as coloured chips,
3
+ // each with an icon where its words suggest one. The colour comes from the tag's name, so one tag
4
+ // reads the same colour on every row, page and app with nothing stored.
5
+ import { Baby, BookOpen, Bot, Briefcase, Bug, Building2, Car, Check, Clock, Cloud, Code, Cpu, Database, DollarSign, Dumbbell, Factory, FileText, Film, Flag, Flame, Gamepad2, Globe, GraduationCap, Handshake, Heart, HeartPulse, Hourglass, House, Landmark, LayoutTemplate, Leaf, Lightbulb, Link, Lock, MapPin, Megaphone, Music, Network, Palette, Plane, Rocket, Server, Shield, ShoppingCart, Smartphone, Sparkles, Star, TriangleAlert, Truck, Users, Utensils, Wrench, Zap, } from 'lucide-react';
6
+ import { ColorChip } from './color-dot.js';
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
8
+ // Chart colours that read as a hue in light and dark (not the grey `--chart-8`).
9
+ const TAG_COLORS = [1, 2, 3, 4, 5, 6, 7, 9, 10].map((n) => `var(--chart-${n})`);
10
+ /** A tag's colour, picked from its name: the same tag is the same colour everywhere. */
11
+ export function tagColor(tag) {
12
+ const key = tag.trim().toLowerCase();
13
+ let h = 2166136261;
14
+ for (let i = 0; i < key.length; i++)
15
+ h = Math.imul(h ^ key.charCodeAt(i), 16777619);
16
+ return TAG_COLORS[(h >>> 0) % TAG_COLORS.length];
17
+ }
18
+ // Words to icons, most specific first: a tag takes the icon of the first entry any of its words
19
+ // matches (`data-security` is a shield, `ai-buildout` a server).
20
+ const TAG_ICONS = [
21
+ [/^(fav|favou?rites?|starred|top|pick)$/, Star],
22
+ [/^(cyber|cybersecurity|security|secure|infosec|appsec|privacy)$/, Shield],
23
+ [/^(auth|identity|iam|zero|zerotrust|crypto|encryption)$/, Lock],
24
+ [/^(buildout|datacenters?|dc|infra|infrastructure|servers?|hosting|compute)$/, Server],
25
+ [/^(chips?|semis?|semiconductors?|hardware|gpus?|silicon)$/, Cpu],
26
+ [/^(ai|ml|llm|genai|agents?|agentic)$/, Sparkles],
27
+ [/^(bots?|robots?|robotics|automation)$/, Bot],
28
+ [/^(data|database|db|analytics|warehouse|sql|etl)$/, Database],
29
+ [/^(cloud|saas|serverless)$/, Cloud],
30
+ [/^(network|networking|edge|cdn)$/, Network],
31
+ [/^(bigtech|faang|enterprise|corp|corporate|company)$/, Building2],
32
+ [/^(dev|devtools|developer|developers|code|coding|engineering|software|platform)$/, Code],
33
+ [/^(startup|startups|growth|launch)$/, Rocket],
34
+ [/^(fintech|finance|financial|bank|banking|money|pay|payments|budget)$/, DollarSign],
35
+ [/^(gov|government|public|federal|civic)$/, Landmark],
36
+ [/^(health|healthcare|medical|bio|biotech|pharma)$/, HeartPulse],
37
+ [/^(fitness|gym|workout|sports?)$/, Dumbbell],
38
+ [/^(energy|power|electric|ev|battery|utilities)$/, Zap],
39
+ [/^(climate|green|sustainability|eco|garden|plants?)$/, Leaf],
40
+ [/^(manufacturing|industrial|factory)$/, Factory],
41
+ [/^(mobile|phone|ios|android|apps?)$/, Smartphone],
42
+ [/^(remote|global|international|web|online)$/, Globe],
43
+ [/^(local|onsite|hybrid|office|nearby)$/, MapPin],
44
+ [/^(home|house|family)$/, House],
45
+ [/^(kids?|baby|children)$/, Baby],
46
+ [/^(team|people|social|community|hr)$/, Users],
47
+ [/^(jobs?|work|career|hiring)$/, Briefcase],
48
+ [/^(edu|education|learning|school|course|courses|training)$/, GraduationCap],
49
+ [/^(docs?|documents?|guides?|pdf|files?)$/, FileText],
50
+ [/^(templates?|layouts?)$/, LayoutTemplate],
51
+ [/^(how|howto|tips?|ideas?)$/, Lightbulb],
52
+ [/^(partners?|partnership|vendors?)$/, Handshake],
53
+ [/^(links?|urls?|bookmarks?)$/, Link],
54
+ [/^(books?|reading|research)$/, BookOpen],
55
+ [/^(games?|gaming)$/, Gamepad2],
56
+ [/^(music|audio)$/, Music],
57
+ [/^(film|video|movies?|media|streaming)$/, Film],
58
+ [/^(design|art|creative)$/, Palette],
59
+ [/^(marketing|ads|sales|pr)$/, Megaphone],
60
+ [/^(shop|shopping|retail|ecommerce|commerce)$/, ShoppingCart],
61
+ [/^(food|recipes?|dinner|lunch|breakfast|dessert|cooking)$/, Utensils],
62
+ [/^(travel|trip|flights?)$/, Plane],
63
+ [/^(cars?|auto|automotive)$/, Car],
64
+ [/^(logistics|shipping|delivery|supply)$/, Truck],
65
+ [/^(tools?|repair|maintenance|fix)$/, Wrench],
66
+ [/^(bugs?|issues?)$/, Bug],
67
+ [/^(urgent|hot|asap)$/, Flame],
68
+ [/^(risk|warning|alert|blocked)$/, TriangleAlert],
69
+ [/^(done|ok|approved|verified)$/, Check],
70
+ [/^(waiting|pending|later|someday)$/, Hourglass],
71
+ [/^(daily|weekly|monthly|recurring|time)$/, Clock],
72
+ [/^(goal|goals|milestone|priority)$/, Flag],
73
+ [/^(love|loved|liked)$/, Heart],
74
+ ];
75
+ const iconCache = new Map();
76
+ /** A tag's icon from its words (`cybersecurity` a shield, `ai` sparkles), or none when no word fits. */
77
+ export function tagIcon(tag) {
78
+ const key = tag.trim().toLowerCase();
79
+ let icon = iconCache.get(key);
80
+ if (icon === undefined) {
81
+ const words = key.split(/[^a-z0-9]+/).filter(Boolean);
82
+ icon = TAG_ICONS.find(([re]) => words.some((w) => re.test(w)))?.[1] ?? null;
83
+ iconCache.set(key, icon);
84
+ }
85
+ return icon ?? undefined;
86
+ }
87
+ /**
88
+ * A row's tags as chips on one line: each in its colour with its icon. Past `max` the rest fold
89
+ * into a `+N` chip whose hover names them all, so a table row never wraps. `colorOf` and `iconOf`
90
+ * override the name-picked colour and icon (an app's own label colours).
91
+ */
92
+ export function TagChips({ tags, max = 3, colorOf, iconOf, empty, }) {
93
+ if (!tags.length)
94
+ return _jsx("span", { className: "text-muted-foreground", children: empty ?? '—' });
95
+ const shown = tags.slice(0, max);
96
+ const rest = tags.length - shown.length;
97
+ const chips = (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 overflow-hidden", children: [shown.map((t) => (_jsx(ColorChip, { color: colorOf?.(t) ?? tagColor(t), icon: iconOf?.(t) ?? tagIcon(t), title: t, className: "max-w-full min-w-9 shrink", children: t }, t))), rest > 0 && _jsxs(ColorChip, { children: ["+", rest] })] }));
98
+ if (!rest)
99
+ return chips;
100
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: chips }), _jsx(TooltipContent, { className: "max-w-md whitespace-normal", children: tags.join(', ') })] }));
101
+ }
@@ -25,6 +25,13 @@ export interface ViewField<T> {
25
25
  topic?: string;
26
26
  /** Every value a row has for filtering, when it has several (a symbol in Holdings and Watchlist). */
27
27
  values?: (row: T) => string[];
28
+ /**
29
+ * The values are tags (a row's several free-form labels): the cell draws each of `values` as a
30
+ * chip in its colour with its icon (`TagChips`), never a comma list. The colour and icon come
31
+ * from the tag's name unless `color` or `iconOf` gives them. Without `values` the text value is
32
+ * split on commas.
33
+ */
34
+ tags?: boolean;
28
35
  format: ViewFormat;
29
36
  /** How the value is made, shown under the field in the Edit dialog. */
30
37
  formula?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.172",
3
+ "version": "0.1.174",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.172",
52
+ "@columenlabs/lumen-ui-types": "0.1.174",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",