@lotics/ui 42.0.0 → 42.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.
@@ -20,6 +20,21 @@ export interface SummaryLineItem {
20
20
  trend?: number | null;
21
21
  /** What this metric means — the ⓘ next to the value opens it in a popover. */
22
22
  info?: string;
23
+ /**
24
+ * Swatch colour, when this line is ALSO the legend for a chart above it —
25
+ * typically a `StackedProgressBar` whose segments are these same aggregates.
26
+ *
27
+ * It exists so a bar can be labelled without giving up what this component
28
+ * already carries. `LegendItem` annotates a chart but holds no `info`, so
29
+ * reaching for it costs the popovers that explain what a bucket includes and
30
+ * what it must not be added to — and those explanations are the reason several
31
+ * of these lines are trustworthy. One swatch here keeps identity off colour
32
+ * alone, which is what a legend is for.
33
+ *
34
+ * Pass the SAME value the segment uses. Omit it and the item renders exactly
35
+ * as it always has.
36
+ */
37
+ color?: string;
23
38
  }
24
39
 
25
40
  export interface SummaryLineProps {
@@ -55,6 +70,7 @@ export function SummaryLine(props: SummaryLineProps) {
55
70
  <View style={styles.row}>
56
71
  {items.map((item) => (
57
72
  <View key={item.label} style={styles.item}>
73
+ {item.color ? <View style={[styles.swatch, { backgroundColor: item.color }]} /> : null}
58
74
  <Metric
59
75
  value={item.value}
60
76
  format={item.format}
@@ -89,4 +105,11 @@ const styles = StyleSheet.create({
89
105
  alignItems: "center",
90
106
  gap: 6,
91
107
  },
108
+ // Matches `LegendItem`'s swatch, because the two annotate the same charts and
109
+ // a reader should not be able to tell which component drew the legend.
110
+ swatch: {
111
+ width: 8,
112
+ height: 8,
113
+ borderRadius: 2,
114
+ },
92
115
  });
package/src/theme.tsx CHANGED
@@ -1,61 +1,24 @@
1
- import { createContext, useContext, type ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
+ import { LoticsThemeContext, type LoticsTheme } from "./theme_context";
2
3
 
3
- /**
4
- * Default platform accent — refined OKLCH blue. Used by chart fills,
5
- * focus rings, and any primitive that asks "what's the brand color".
6
- * Apps that need a different accent wrap their root in `LoticsThemeProvider`.
7
- *
8
- * Why OKLCH instead of hex? Perceptual uniformity — `oklch(0.6 0.118 250)`
9
- * sits at the same perceptual lightness/saturation as the `oklch(0.6 0.118
10
- * 184.704)` (teal) chị's workspace uses, just shifted in hue. Hex shifts
11
- * lightness as hue rotates and the eye picks it up as inconsistency.
12
- */
13
- export const DEFAULT_ACCENT = "oklch(0.6 0.118 250)";
14
-
15
- interface LoticsTheme {
16
- /** Single brand accent. Chart fills, hero CTAs, focus rings. */
17
- accent: string;
18
- }
19
-
20
- const LoticsThemeContext = createContext<LoticsTheme>({ accent: DEFAULT_ACCENT });
4
+ export { DEFAULT_ACCENT, useLoticsTheme, THEME_VARS, type LoticsTheme } from "./theme_context";
21
5
 
22
- interface LoticsThemeProviderProps {
23
- /** Brand accent. Overrides the platform default. Accepts any CSS color
24
- * value (OKLCH recommended, hex / hsl also fine). */
25
- accent?: string;
6
+ interface LoticsThemeProviderProps extends LoticsTheme {
26
7
  children: ReactNode;
27
8
  }
28
9
 
29
10
  /**
30
- * App-root provider that supplies brand tokens to @lotics/ui primitives.
31
- * Wrap your top-level app element to override the platform defaults:
11
+ * App-root provider the NATIVE half.
32
12
  *
33
- * // src/main.tsx
34
- * <LoticsThemeProvider accent="oklch(0.6 0.118 184.704)">
35
- * <App />
36
- * </LoticsThemeProvider>
13
+ * On native there are no CSS variables, and `colors` is a literal table, so
14
+ * there is nothing to declare: the provider publishes the values for anything
15
+ * that reads them through `useLoticsTheme()` and renders its children
16
+ * untouched. The web half (`theme.web.tsx`) is where a theme actually paints.
37
17
  *
38
- * Components that consume theme tokens use `useLoticsTheme()`. Each
39
- * primitive also accepts a per-instance `color` prop for one-off
40
- * customization without needing a different provider.
41
- *
42
- * Scope is intentionally narrow — accent only. Semantic colors (success,
43
- * danger) already work via existing `colors.green[600]` etc. Adding more
44
- * theme tokens requires a real product reason.
18
+ * Deliberately NOT a `View`: a wrapper here would insert a layout box into every
19
+ * themed app's tree on the one platform that gains nothing from it.
45
20
  */
46
21
  export function LoticsThemeProvider(props: LoticsThemeProviderProps) {
47
- const accent = props.accent ?? DEFAULT_ACCENT;
48
- return (
49
- <LoticsThemeContext.Provider value={{ accent }}>
50
- {props.children}
51
- </LoticsThemeContext.Provider>
52
- );
53
- }
54
-
55
- /**
56
- * Read the current theme. Primitives that need the accent color call this
57
- * hook; apps don't need it directly (use the provider's prop instead).
58
- */
59
- export function useLoticsTheme(): LoticsTheme {
60
- return useContext(LoticsThemeContext);
22
+ const { children, ...theme } = props;
23
+ return <LoticsThemeContext.Provider value={theme}>{children}</LoticsThemeContext.Provider>;
61
24
  }
@@ -0,0 +1,64 @@
1
+ import { useMemo, type CSSProperties, type ReactNode } from "react";
2
+ import { LoticsThemeContext, THEME_VARS, type LoticsTheme } from "./theme_context";
3
+
4
+ export { DEFAULT_ACCENT, useLoticsTheme, THEME_VARS, type LoticsTheme } from "./theme_context";
5
+
6
+ interface LoticsThemeProviderProps extends LoticsTheme {
7
+ children: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * App-root provider that gives `@lotics/ui` primitives this app's identity:
12
+ *
13
+ * // src/main.tsx
14
+ * <LoticsThemeProvider accent="#0F766E" border="#E3E8E6">
15
+ * <App />
16
+ * </LoticsThemeProvider>
17
+ *
18
+ * It declares the CSS custom properties `colors.web.ts` reads, so every kit
19
+ * component below re-paints without knowing the provider exists. That
20
+ * indirection is the point: the alternative — a context each primitive
21
+ * subscribes to — would have to be threaded through 136 modules and would turn
22
+ * their static stylesheets into per-render inline styles.
23
+ *
24
+ * **It renders a plain `div`, not a `View`, and that is load-bearing.**
25
+ * react-native-web's style compiler only emits properties it knows; a custom
26
+ * property handed to a `View` is silently DROPPED, so the variables never reach
27
+ * the DOM and a themed app renders in the platform defaults with no error
28
+ * anywhere. React DOM, by contrast, writes `--*` inline style keys through
29
+ * verbatim. The failure is invisible rather than loud, which is exactly why the
30
+ * two platforms get two files instead of one clever component.
31
+ *
32
+ * Only a role actually PASSED is declared, so an omitted one leaves its variable
33
+ * undefined and `colors.web.ts`'s inline fallback answers. That is what makes
34
+ * theming additive: a partial theme overrides a part, never resetting the rest
35
+ * to some second set of defaults.
36
+ */
37
+ export function LoticsThemeProvider(props: LoticsThemeProviderProps) {
38
+ const { children, ...theme } = props;
39
+
40
+ // Keyed on the four values, not the object: the call site is normally an
41
+ // inline literal, which would otherwise rebuild this style — and re-render
42
+ // every kit surface under it — on each parent render.
43
+ const style = useMemo(() => {
44
+ const vars: Record<string, string> = {};
45
+ for (const [role, name] of Object.entries(THEME_VARS)) {
46
+ const value = theme[role as keyof LoticsTheme];
47
+ if (value !== undefined) vars[name] = value;
48
+ }
49
+ // `display: contents` — the provider generates NO box. Custom properties
50
+ // inherit down the DOM tree rather than the box tree, so the variables still
51
+ // reach every descendant while the element itself adds no layout at all.
52
+ // The first version was a flex column with `flex: 1`, which silently imposed
53
+ // a layout contract on every app that wrapped its root in this: a provider
54
+ // whose entire job is declaring three strings has no business deciding how its
55
+ // children stack.
56
+ return { display: "contents", ...vars } as CSSProperties;
57
+ }, [theme.accent, theme.background, theme.border]);
58
+
59
+ return (
60
+ <div style={style}>
61
+ <LoticsThemeContext.Provider value={theme}>{children}</LoticsThemeContext.Provider>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext } from "react";
2
+ import { colors } from "./color_tokens";
3
+
4
+ /**
5
+ * The platform's own accent — what every kit surface wears unthemed. Same value
6
+ * `colors.accent` falls back to; named so an app can reference the default
7
+ * explicitly instead of re-typing a literal.
8
+ */
9
+ export const DEFAULT_ACCENT = colors.accent;
10
+
11
+ /** The roles an app may own. See `color_tokens.ts` for why exactly these. */
12
+ export interface LoticsTheme {
13
+ /** The one brand hue. It paints IDENTITY marks — today the avatar's initials
14
+ * disc — and deliberately NOT the focus ring or the primary action, which stay
15
+ * neutral so those read the same in every app. Never a status, a series or a
16
+ * valence: those come from a palette family and carry meaning. */
17
+ accent?: string;
18
+ /** The surface Card, Drawer and Modal paint — 21 sites in the kit. Left white
19
+ * on almost every app; set it only for a deliberately toned surface. */
20
+ background?: string;
21
+ /** Hairlines — 48 sites in the kit, and a register is mostly these lines, so a
22
+ * small change here is felt across a whole screen. */
23
+ border?: string;
24
+ }
25
+
26
+ /**
27
+ * The CSS custom properties `colors.web.ts` reads. Part of the published
28
+ * contract: an app may set them from its own stylesheet instead of the provider.
29
+ */
30
+ export const THEME_VARS: Record<keyof LoticsTheme, string> = {
31
+ accent: "--lotics-accent",
32
+ background: "--lotics-background",
33
+ border: "--lotics-border",
34
+ };
35
+
36
+ export const LoticsThemeContext = createContext<LoticsTheme>({});
37
+
38
+ /**
39
+ * Read the current theme. An app SETS values through the provider's props; this
40
+ * is for a surface that must branch on one (a chart picking a fill). A role the
41
+ * app did not set reads `undefined` — ask `colors.<role>` for what will paint.
42
+ */
43
+ export function useLoticsTheme(): LoticsTheme {
44
+ return useContext(LoticsThemeContext);
45
+ }