@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 +3 -1
- package/src/download.ts +36 -0
- package/src/ring_gauge.tsx +72 -0
- package/src/tokens.ts +15 -5
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/src/download.ts
ADDED
|
@@ -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
|
|
7
|
-
* `colors.zinc[900]` / `colors.background` / etc.
|
|
8
|
-
*
|
|
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.
|
|
90
|
-
*
|
|
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,
|