@lotics/ui 1.9.0 → 1.10.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/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
7
7
  "./colors": "./src/colors.ts",
8
8
  "./mime": "./src/mime.ts",
9
+ "./download": "./src/download.ts",
9
10
  "./file_badge": "./src/file_badge.tsx",
10
11
  "./file_thumbnail": "./src/file_thumbnail.tsx",
11
12
  "./file_gallery_modal": "./src/file_gallery_modal.tsx",
@@ -18,6 +19,7 @@
18
19
  "./trend_chip": "./src/trend_chip.tsx",
19
20
  "./section_card": "./src/section_card.tsx",
20
21
  "./kpi_card": "./src/kpi_card.tsx",
22
+ "./ring_gauge": "./src/ring_gauge.tsx",
21
23
  "./alert_row": "./src/alert_row.tsx",
22
24
  "./stacked_progress_bar": "./src/stacked_progress_bar.tsx",
23
25
  "./legend_item": "./src/legend_item.tsx",
@@ -0,0 +1,36 @@
1
+ // Pure web file-download primitive. Uses fetch+blob+anchor because
2
+ // `window.open(url, "_blank")` is silently dropped by sandboxed iframes
3
+ // (no `allow-popups`), and direct anchor navigation without the `download`
4
+ // attribute doesn't trigger a save dialog for inline MIME types (HTML,
5
+ // JSON, PDFs, etc.).
6
+ //
7
+ // `cache: "no-store"` avoids a CORS cache collision: if a prior <img>
8
+ // loaded the same URL without an Origin header, the cached response
9
+ // (missing Access-Control-Allow-Origin) gets reused for the fetch and
10
+ // fails. Bypassing the cache forces a fresh CORS-aware request.
11
+ //
12
+ // No React Native / @lotics/shared imports — kept pure so both the host
13
+ // frontend and sandboxed custom-code apps can consume via the per-file
14
+ // export without dragging the wider UI surface.
15
+
16
+ export async function downloadFileFromUrl(url: string, filename: string): Promise<void> {
17
+ if (!url) throw new Error("downloadFileFromUrl: empty url");
18
+
19
+ const response = await fetch(url, { cache: "no-store" });
20
+ if (!response.ok) {
21
+ throw new Error(`File download failed: ${response.status} ${response.statusText}`);
22
+ }
23
+
24
+ const blob = await response.blob();
25
+ const blobUrl = URL.createObjectURL(blob);
26
+ try {
27
+ const link = document.createElement("a");
28
+ link.href = blobUrl;
29
+ link.download = filename;
30
+ document.body.appendChild(link);
31
+ link.click();
32
+ link.remove();
33
+ } finally {
34
+ URL.revokeObjectURL(blobUrl);
35
+ }
36
+ }
@@ -0,0 +1,72 @@
1
+ import { View } from "react-native";
2
+ import { colors } from "./colors";
3
+ import { Text } from "./text";
4
+
5
+ export interface RingGaugeProps {
6
+ /** Progress value, 0–100. Clamped to that range. */
7
+ value: number;
8
+ /** Short label under the ring. */
9
+ label: string;
10
+ /** Optional one-line context under the label. */
11
+ caption?: string;
12
+ /** Diameter in px. Default 128. */
13
+ size?: number;
14
+ /** Ring stroke width in px. Default 12. */
15
+ thickness?: number;
16
+ /** Arc color. Default teal accent. */
17
+ color?: string;
18
+ }
19
+
20
+ /**
21
+ * Circular progress gauge — an at-a-glance ring for a 0–100% metric
22
+ * (on-time rate, SLA compliance, win rate). The percentage leads in the
23
+ * center; the track behind it shows the remaining-to-100 context. Pairs
24
+ * with `KPICard` (figures) — use a ring when the number IS a ratio to 100.
25
+ *
26
+ * Implementation: native HTML `<svg>` (like `Sparkline`) — Vite can't
27
+ * resolve react-native-svg's native paths, and the `<View>` wrapper
28
+ * preserves the RN layout surface. The arc starts at 12 o'clock
29
+ * (`rotate(-90)`) and grows clockwise via `strokeDasharray`.
30
+ */
31
+ export function RingGauge(props: RingGaugeProps) {
32
+ const { value, label, caption, size = 140, thickness = 10, color = colors.teal[600] } = props;
33
+ const clamped = Math.max(0, Math.min(100, value));
34
+ const center = size / 2;
35
+ const radius = (size - thickness) / 2;
36
+ const circumference = 2 * Math.PI * radius;
37
+ const dash = (clamped / 100) * circumference;
38
+
39
+ return (
40
+ <View style={{ alignItems: "center", gap: 12 }}>
41
+ <View style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
42
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ position: "absolute" }}>
43
+ <circle cx={center} cy={center} r={radius} fill="none" stroke={colors.zinc[200]} strokeWidth={thickness} />
44
+ <circle
45
+ cx={center}
46
+ cy={center}
47
+ r={radius}
48
+ fill="none"
49
+ stroke={color}
50
+ strokeWidth={thickness}
51
+ strokeLinecap="round"
52
+ strokeDasharray={`${dash} ${circumference}`}
53
+ transform={`rotate(-90 ${center} ${center})`}
54
+ />
55
+ </svg>
56
+ <Text size="xxl" weight="semibold">
57
+ {Math.round(clamped)}%
58
+ </Text>
59
+ </View>
60
+ <View style={{ alignItems: "center", gap: 2 }}>
61
+ <Text size="sm" weight="medium">
62
+ {label}
63
+ </Text>
64
+ {caption ? (
65
+ <Text size="xs" color="muted">
66
+ {caption}
67
+ </Text>
68
+ ) : null}
69
+ </View>
70
+ </View>
71
+ );
72
+ }
package/src/tokens.ts CHANGED
@@ -3,9 +3,16 @@
3
3
  * Lotics surfaces (parent app, custom_code iframe apps, browser extension).
4
4
  *
5
5
  * The colors come from `colors.ts` (re-exported below) — same module
6
- * the parent app's React Native components consume via
7
- * `colors.zinc[900]` / `colors.background` / etc. The iframe runtime
8
- * emits these same values as CSS variables via {@link getCssVariables}.
6
+ * BOTH the parent app's and custom-code apps' components consume via
7
+ * `colors.zinc[900]` / `colors.background` / etc. Custom-code apps use
8
+ * `@lotics/ui` components directly (rendered through react-native-web; the
9
+ * starter scaffold wires the alias — see `docs/apps.md`), so they self-theme
10
+ * via the same `colors` module with no CSS variables involved.
11
+ *
12
+ * {@link getCssVariables} is an OPT-IN helper that serializes these tokens to
13
+ * `--lotics-*` CSS variables for apps that hand-roll plain DOM/CSS instead of
14
+ * using `@lotics/ui` components. Nothing injects them automatically — the
15
+ * runtime does NOT emit them, and the component path does not need them.
9
16
  *
10
17
  * **CSS variable naming mirrors the parent's TS references 1:1**:
11
18
  * colors.zinc[900] → var(--lotics-zinc-900)
@@ -86,8 +93,11 @@ export const radius = {
86
93
 
87
94
  /**
88
95
  * Emit a CSS string defining all design tokens as `:root` custom
89
- * properties. Used by the iframe runtime so plain-DOM components can
90
- * consume the same palette as the parent app's React Native components.
96
+ * properties. OPT-IN: an app that hand-rolls plain DOM/CSS (instead of
97
+ * using `@lotics/ui` components, which self-theme via the `colors` module)
98
+ * can inject this to consume the same palette. Nothing calls it
99
+ * automatically — there is no runtime that emits it; the component path
100
+ * doesn't need it.
91
101
  *
92
102
  * Variable names mirror `colors` 1:1 — palette entries (zinc, red,
93
103
  * etc.) emit as `--lotics-<name>-<shade>`, top-level entries (black,