From 2ae9740c385d7159f3a3709c3dc8482e80930204 Mon Sep 17 00:00:00 2001 From: yanchuan Date: Fri, 30 Oct 2026 11:28:18 +0800 Subject: [PATCH] feat: add IPv4 and IPv6 CIDR calculator --- Cargo.lock | 10 + Cargo.toml | 1 + apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/plugins.rs | 1 + plugins/cidr-calculator/backend/Cargo.toml | 12 + plugins/cidr-calculator/backend/src/lib.rs | 325 +++++++++++++++++++++ plugins/cidr-calculator/icon.svg | 1 + plugins/cidr-calculator/locales/en-US.json | 47 +++ plugins/cidr-calculator/locales/zh-CN.json | 47 +++ plugins/cidr-calculator/manifest.json | 32 ++ plugins/cidr-calculator/ui/package.json | 27 ++ plugins/cidr-calculator/ui/src/index.tsx | 71 +++++ plugins/cidr-calculator/ui/tsconfig.json | 4 + pnpm-lock.yaml | 28 ++ 14 files changed, 607 insertions(+) create mode 100644 plugins/cidr-calculator/backend/Cargo.toml create mode 100644 plugins/cidr-calculator/backend/src/lib.rs create mode 100644 plugins/cidr-calculator/icon.svg create mode 100644 plugins/cidr-calculator/locales/en-US.json create mode 100644 plugins/cidr-calculator/locales/zh-CN.json create mode 100644 plugins/cidr-calculator/manifest.json create mode 100644 plugins/cidr-calculator/ui/package.json create mode 100644 plugins/cidr-calculator/ui/src/index.tsx create mode 100644 plugins/cidr-calculator/ui/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 7158042b..80a33cfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9724,6 +9724,15 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "tfp-cidr-calculator" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tf-plugin-api", +] + [[package]] name = "tfp-context-menu" version = "0.1.0" @@ -10335,6 +10344,7 @@ dependencies = [ "tfp-barcode", "tfp-batch-rename", "tfp-certificate", + "tfp-cidr-calculator", "tfp-context-menu", "tfp-disk-usage", "tfp-dns-lookup", diff --git a/Cargo.toml b/Cargo.toml index 6a6dc27d..0e4a19c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ tfp-archive = { path = "plugins/archive/backend" } tfp-background-remover = { path = "plugins/background-remover/backend" } tfp-barcode = { path = "plugins/barcode/backend" } tfp-batch-rename = { path = "plugins/batch-rename/backend" } +tfp-cidr-calculator = { path = "plugins/cidr-calculator/backend" } tfp-certificate = { path = "plugins/certificate/backend" } tfp-dns-lookup = { path = "plugins/dns-lookup/backend" } tfp-disk-usage = { path = "plugins/disk-usage/backend" } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index fd9c37dd..a2f953b1 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ tfp-archive.workspace = true tfp-background-remover.workspace = true tfp-barcode.workspace = true tfp-batch-rename.workspace = true +tfp-cidr-calculator.workspace = true tfp-certificate.workspace = true tfp-dns-lookup.workspace = true tfp-disk-usage.workspace = true diff --git a/apps/desktop/src-tauri/src/plugins.rs b/apps/desktop/src-tauri/src/plugins.rs index 16b4c0b3..a73696b7 100644 --- a/apps/desktop/src-tauri/src/plugins.rs +++ b/apps/desktop/src-tauri/src/plugins.rs @@ -22,6 +22,7 @@ pub fn builtin_for(os: &str) -> PluginRegistry { os, ); registry.register_for(Arc::new(tfp_dns_lookup::DnsLookup::default()), os); + registry.register_for(Arc::new(tfp_cidr_calculator::CidrCalculator::default()), os); registry.register_for(Arc::new(tfp_system_monitor::SystemMonitor::default()), os); registry.register_for(Arc::new(tfp_ocr::Ocr::default()), os); registry.register_for(Arc::new(tfp_port_manager::PortManager::default()), os); diff --git a/plugins/cidr-calculator/backend/Cargo.toml b/plugins/cidr-calculator/backend/Cargo.toml new file mode 100644 index 00000000..5400b7e4 --- /dev/null +++ b/plugins/cidr-calculator/backend/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tfp-cidr-calculator" +description = "ToolForge plugin: IPv4/IPv6 CIDR calculator" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +tf-plugin-api.workspace = true diff --git a/plugins/cidr-calculator/backend/src/lib.rs b/plugins/cidr-calculator/backend/src/lib.rs new file mode 100644 index 00000000..11b77761 --- /dev/null +++ b/plugins/cidr-calculator/backend/src/lib.rs @@ -0,0 +1,325 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tf_plugin_api::{ + Manifest, PluginError, PluginResult, ToolPlugin, parse_args, to_value, unknown_function, +}; + +const MANIFEST: &str = include_str!("../../manifest.json"); +const PAGE_SIZE: u128 = 64; +const TWO_TO_128: &str = "340282366920938463463374607431768211456"; + +pub struct CidrCalculator { + manifest: Manifest, +} +impl Default for CidrCalculator { + fn default() -> Self { + Self { + manifest: Manifest::from_static(MANIFEST), + } + } +} +#[derive(Deserialize)] +struct CalculateArgs { + cidr: String, +} +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SplitArgs { + cidr: String, + prefix: u8, + #[serde(default)] + page: u64, +} +#[derive(Clone, Copy)] +struct Cidr { + ip: u128, + prefix: u8, + bits: u8, +} +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Network { + cidr: String, + version: u8, + network: String, + broadcast: String, + mask: String, + wildcard: String, + first_host: String, + last_host: String, + addresses: String, + usable_hosts: String, + prefix: u8, +} +fn parse_cidr(input: &str) -> PluginResult { + let (ip, prefix) = input + .trim() + .split_once('/') + .ok_or_else(|| PluginError::new("cidr.invalid"))?; + let ip: IpAddr = ip.parse().map_err(|_| PluginError::new("cidr.invalid"))?; + if prefix.is_empty() || !prefix.bytes().all(|b| b.is_ascii_digit()) { + return Err(PluginError::new("cidr.invalid")); + } + let prefix: u8 = prefix + .parse() + .map_err(|_| PluginError::new("cidr.invalid"))?; + let (ip, bits) = match ip { + IpAddr::V4(ip) => (u128::from(u32::from(ip)), 32), + IpAddr::V6(ip) => (u128::from(ip), 128), + }; + if prefix > bits { + return Err(PluginError::new("cidr.invalid")); + } + Ok(Cidr { ip, prefix, bits }) +} +fn address(value: u128, bits: u8) -> String { + if bits == 32 { + Ipv4Addr::from(value as u32).to_string() + } else { + Ipv6Addr::from(value).to_string() + } +} +fn mask(bits: u8, prefix: u8) -> u128 { + let all = if bits == 32 { + u32::MAX as u128 + } else { + u128::MAX + }; + if prefix == 0 { + 0 + } else { + all << (bits - prefix) & all + } +} +fn count(power: u8) -> String { + if power == 128 { + TWO_TO_128.to_owned() + } else { + (1u128 << power).to_string() + } +} +fn network(cidr: Cidr) -> Network { + let Cidr { ip, prefix, bits } = cidr; + let all = if bits == 32 { + u32::MAX as u128 + } else { + u128::MAX + }; + let mask = mask(bits, prefix); + let base = ip & mask; + let end = base | (all ^ mask); + let addresses = count(bits - prefix); + // IPv4 /31 point-to-point links and /32 hosts use all addresses. IPv6 has no broadcast address. + let (first, last, usable) = if bits == 32 && prefix < 31 { + ( + base + 1, + end - 1, + ((1u128 << (32 - prefix)) - 2).to_string(), + ) + } else { + (base, end, addresses.clone()) + }; + Network { + cidr: format!("{}/{prefix}", address(base, bits)), + version: if bits == 32 { 4 } else { 6 }, + network: address(base, bits), + broadcast: address(end, bits), + mask: address(mask, bits), + wildcard: address(all ^ mask, bits), + first_host: address(first, bits), + last_host: address(last, bits), + addresses, + usable_hosts: usable, + prefix, + } +} +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct SplitReport { + total: String, + page: u64, + has_next: bool, + subnets: Vec, +} +fn split(args: SplitArgs) -> PluginResult { + let parent = parse_cidr(&args.cidr)?; + if args.prefix < parent.prefix || args.prefix > parent.bits { + return Err(PluginError::new("cidr.invalid_prefix")); + } + let difference = args.prefix - parent.prefix; + let total = count(difference); + let total_number = if difference == 128 { + None + } else { + Some(1u128 << difference) + }; + let start = u128::from(args.page) * PAGE_SIZE; + if total_number.is_some_and(|total| start >= total) { + return Err(PluginError::new("cidr.invalid_page")); + } + let length = total_number.map_or(PAGE_SIZE, |total| (total - start).min(PAGE_SIZE)); + let base = parent.ip & mask(parent.bits, parent.prefix); + let shift = parent.bits - args.prefix; + let subnets = (0..length) + .map(|offset| { + let index = start + offset; + let delta = if shift == 128 { 0 } else { index << shift }; + network(Cidr { + ip: base + delta, + prefix: args.prefix, + bits: parent.bits, + }) + }) + .collect(); + let has_next = args.page < u64::MAX && total_number.is_none_or(|total| start + length < total); + Ok(SplitReport { + total, + page: args.page, + has_next, + subnets, + }) +} +impl ToolPlugin for CidrCalculator { + fn manifest(&self) -> &Manifest { + &self.manifest + } + fn call(&self, function: &str, args: Value) -> PluginResult { + match function { + "calculate" => { + let args: CalculateArgs = parse_args(args)?; + to_value(network(parse_cidr(&args.cidr)?)) + } + "split" => to_value(split(parse_args(args)?)?), + other => Err(unknown_function(other)), + } + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn calculates_ipv4_boundaries() { + let n = network(parse_cidr("192.168.1.42/24").unwrap()); + assert_eq!( + ( + n.cidr.as_str(), + n.broadcast.as_str(), + n.first_host.as_str(), + n.last_host.as_str(), + n.usable_hosts.as_str() + ), + ( + "192.168.1.0/24", + "192.168.1.255", + "192.168.1.1", + "192.168.1.254", + "254" + ) + ); + let n = network(parse_cidr("0.0.0.0/0").unwrap()); + assert_eq!( + ( + n.addresses.as_str(), + n.usable_hosts.as_str(), + n.broadcast.as_str() + ), + ("4294967296", "4294967294", "255.255.255.255") + ); + assert_eq!( + network(parse_cidr("255.255.255.255/31").unwrap()).usable_hosts, + "2" + ); + assert_eq!( + network(parse_cidr("255.255.255.255/32").unwrap()).first_host, + "255.255.255.255" + ); + } + #[test] + fn calculates_ipv6_boundaries() { + let n = network(parse_cidr("2001:db8:1234:5678::abcd/64").unwrap()); + assert_eq!( + ( + n.cidr.as_str(), + n.first_host.as_str(), + n.last_host.as_str(), + n.addresses.as_str() + ), + ( + "2001:db8:1234:5678::/64", + "2001:db8:1234:5678::", + "2001:db8:1234:5678:ffff:ffff:ffff:ffff", + "18446744073709551616" + ) + ); + let n = network(parse_cidr("::1/0").unwrap()); + assert_eq!(n.addresses, TWO_TO_128); + assert_eq!(n.broadcast, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + assert_eq!(network(parse_cidr("::1/128").unwrap()).usable_hosts, "1"); + } + #[test] + fn validates_and_pages_splits() { + for input in [ + "192.168.1.1", + "192.168.1.256/24", + "1.2.3.4/33", + "::1/129", + "1.2.3.4/-1", + ] { + assert!(parse_cidr(input).is_err()); + } + let s = split(SplitArgs { + cidr: "10.1.2.3/16".into(), + prefix: 24, + page: 1, + }) + .unwrap(); + assert_eq!(s.total, "256"); + assert_eq!(s.subnets[0].cidr, "10.1.64.0/24"); + assert_eq!(s.subnets[63].cidr, "10.1.127.0/24"); + let s = split(SplitArgs { + cidr: "2001:db8::/120".into(), + prefix: 128, + page: 3, + }) + .unwrap(); + assert_eq!(s.subnets[0].cidr, "2001:db8::c0/128"); + assert!(!s.has_next); + let s = split(SplitArgs { + cidr: "::/0".into(), + prefix: 128, + page: 0, + }) + .unwrap(); + assert_eq!(s.total, TWO_TO_128); + assert!(s.has_next); + assert_eq!( + split(SplitArgs { + cidr: "::/0".into(), + prefix: 0, + page: 0 + }) + .unwrap() + .subnets[0] + .cidr, + "::/0" + ); + assert!( + split(SplitArgs { + cidr: "10.0.0.0/24".into(), + prefix: 23, + page: 0 + }) + .is_err() + ); + assert!( + split(SplitArgs { + cidr: "::/128".into(), + prefix: 128, + page: 1 + }) + .is_err() + ); + } +} diff --git a/plugins/cidr-calculator/icon.svg b/plugins/cidr-calculator/icon.svg new file mode 100644 index 00000000..38f50bb8 --- /dev/null +++ b/plugins/cidr-calculator/icon.svg @@ -0,0 +1 @@ + diff --git a/plugins/cidr-calculator/locales/en-US.json b/plugins/cidr-calculator/locales/en-US.json new file mode 100644 index 00000000..c0e5639e --- /dev/null +++ b/plugins/cidr-calculator/locales/en-US.json @@ -0,0 +1,47 @@ +{ + "name": "CIDR Calculator", + "description": "Calculate IPv4 and IPv6 network details and plan subnets", + "help": [ + "Enter an IPv4 or IPv6 address and prefix, for example 192.168.1.10/24 or 2001:db8::1/64.", + "Subnet planning divides the network into equal-sized subnets and displays results in pages.", + "IPv4 /31 uses both addresses for point-to-point links; /32 represents one host. IPv6 displays the full address range." + ], + "input": { + "label": "IP CIDR", + "calculate": "Calculate", + "hint": "Enter an IPv4 or IPv6 address and prefix, such as 192.168.1.10/24 or 2001:db8::1/64" + }, + "empty": "Enter a CIDR to calculate network details", + "result": { + "title": "Network details", + "network": "Network address", + "broadcast": "Broadcast / last address", + "mask": "Subnet mask", + "wildcard": "Wildcard mask", + "firstHost": "First usable address", + "lastHost": "Last usable address", + "addresses": "Total addresses", + "usableHosts": "Usable hosts", + "hostHint": "/31 counts both addresses for point-to-point links; /32 represents a single host.", + "lastAddress": "Last address", + "ipv6Hint": "IPv6 has no broadcast address. The range and count include all addresses; actual assignability depends on network configuration." + }, + "plan": { + "title": "Subnet planning", + "hint": "Divide {{cidr}} into equal subnets. Choose a prefix from /{{prefix}} to /{{max}}.", + "prefix": "Target prefix length", + "split": "Split network", + "count": "{{total}} subnets", + "previous": "Previous", + "next": "Next", + "page": "Page {{page}}", + "subnet": "Subnet", + "range": "Usable address range", + "usableHosts": "Usable hosts" + }, + "errors": { + "cidr.invalid": "Enter a valid IPv4 or IPv6 CIDR, such as 192.168.1.10/24 or 2001:db8::1/64.", + "cidr.invalid_prefix": "The target prefix must be at least the original prefix and no greater than the address family maximum.", + "cidr.invalid_page": "The subnet page is out of range." + } +} diff --git a/plugins/cidr-calculator/locales/zh-CN.json b/plugins/cidr-calculator/locales/zh-CN.json new file mode 100644 index 00000000..6ce6eb3b --- /dev/null +++ b/plugins/cidr-calculator/locales/zh-CN.json @@ -0,0 +1,47 @@ +{ + "name": "CIDR 计算器", + "description": "计算 IPv4 和 IPv6 网段信息并规划子网划分", + "help": [ + "输入 IPv4 或 IPv6 地址与前缀长度,例如 192.168.1.10/24 或 2001:db8::1/64。", + "子网规划按指定前缀等分网段,结果分页显示。", + "IPv4 /31 按点对点链路计算两个可用地址,/32 按单主机计算;IPv6 显示完整地址范围。" + ], + "input": { + "label": "IP CIDR", + "calculate": "计算", + "hint": "输入 IPv4 或 IPv6 地址和前缀,例如 192.168.1.10/24 或 2001:db8::1/64" + }, + "empty": "输入 CIDR 后开始计算网段信息", + "result": { + "title": "网段信息", + "network": "网络地址", + "broadcast": "广播地址 / 末地址", + "mask": "子网掩码", + "wildcard": "通配符掩码", + "firstHost": "首个可用地址", + "lastHost": "最后可用地址", + "addresses": "地址总数", + "usableHosts": "可用主机数", + "hostHint": "/31 按点对点链路计算两个可用地址;/32 为单个主机地址。", + "lastAddress": "末地址", + "ipv6Hint": "IPv6 没有广播地址;范围和数量包含网段内全部地址,实际可分配情况取决于网络配置。" + }, + "plan": { + "title": "子网规划", + "hint": "将 {{cidr}} 等分为更小的子网,目标前缀须在 /{{prefix}} 至 /{{max}} 之间。", + "prefix": "目标前缀长度", + "split": "划分子网", + "count": "共 {{total}} 个子网", + "previous": "上一页", + "next": "下一页", + "page": "第 {{page}} 页", + "subnet": "子网", + "range": "可用地址范围", + "usableHosts": "可用主机数" + }, + "errors": { + "cidr.invalid": "请输入有效的 IPv4 或 IPv6 CIDR,例如 192.168.1.10/24 或 2001:db8::1/64。", + "cidr.invalid_prefix": "目标前缀必须大于或等于原网段前缀,且不超过该地址族的最大长度。", + "cidr.invalid_page": "子网页码超出范围。" + } +} diff --git a/plugins/cidr-calculator/manifest.json b/plugins/cidr-calculator/manifest.json new file mode 100644 index 00000000..a0fc1c1b --- /dev/null +++ b/plugins/cidr-calculator/manifest.json @@ -0,0 +1,32 @@ +{ + "id": "org.devlive.toolforge.cidr-calculator", + "version": "0.1.0", + "name": "i18n:name", + "description": "i18n:description", + "category": "network", + "keywords": [ + "cidr", + "subnet", + "ipv4", + "ipv6", + "ip", + "netmask", + "network", + "网段", + "子网", + "掩码" + ], + "accent": "cyan", + "locales": [ + "zh-CN", + "en-US" + ], + "permissions": [ + "clipboard:write" + ], + "functions": { + "calculate": {}, + "split": {} + }, + "sensitive": false +} diff --git a/plugins/cidr-calculator/ui/package.json b/plugins/cidr-calculator/ui/package.json new file mode 100644 index 00000000..0fb02435 --- /dev/null +++ b/plugins/cidr-calculator/ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@toolforge-plugin/cidr-calculator", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.tsx" + }, + "scripts": { + "typecheck": "tsc -p tsconfig.json" + }, + "peerDependencies": { + "react": "^19" + }, + "dependencies": { + "@toolforge/plugin-ui-sdk": "workspace:*", + "@toolforge/ui": "workspace:*", + "lucide-react": "1.47.0", + "react-i18next": "17.0.15" + }, + "devDependencies": { + "@types/react": "19.3.0", + "react": "19.3.0", + "typescript": "5.9.3", + "vite": "8.3.0" + } +} diff --git a/plugins/cidr-calculator/ui/src/index.tsx b/plugins/cidr-calculator/ui/src/index.tsx new file mode 100644 index 00000000..7d93d71c --- /dev/null +++ b/plugins/cidr-calculator/ui/src/index.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react' +import { Button, Input, Panel } from '@toolforge/ui' +import { CopyButton, useLaunchInput, usePlugin } from '@toolforge/plugin-ui-sdk' +import { Calculator, Network, Search } from 'lucide-react' + +type NetworkInfo = { cidr: string; version: number; network: string; broadcast: string; mask: string; wildcard: string; firstHost: string; lastHost: string; addresses: string; usableHosts: string; prefix: number } +type SplitReport = { total: string; page: number; hasNext: boolean; subnets: NetworkInfo[] } + +export default function CidrCalculator() { + const { t, call, errorMessage } = usePlugin() + const [input, setInput] = useState('192.168.1.0/24') + useLaunchInput(setInput) + const [result, setResult] = useState(null) + const [prefix, setPrefix] = useState('26') + const [plan, setPlan] = useState(null) + const [error, setError] = useState(null) + const [planError, setPlanError] = useState(null) + const [busy, setBusy] = useState(false) + const [planning, setPlanning] = useState(false) + const calculate = async () => { + setBusy(true); setError(null); setResult(null); setPlan(null); setPlanError(null) + try { + const value = await call('calculate', { cidr: input }) + setResult(value); setPrefix(String(Math.min(value.prefix + 2, value.version === 4 ? 32 : 128))) + } catch (reason) { setError(reason) } finally { setBusy(false) } + } + const split = async (page = 0) => { + if (!result) return + setPlanning(true); setPlanError(null); setPlan(null) + try { setPlan(await call('split', { cidr: result.cidr, prefix: Number(prefix), page })) } + catch (reason) { setPlanError(reason) } finally { setPlanning(false) } + } + const fields: [string, string][] = result ? [ + ['network', result.network], [result.version === 4 ? 'broadcast' : 'lastAddress', result.broadcast], ['mask', result.mask], ['wildcard', result.wildcard], + ['firstHost', result.firstHost], ['lastHost', result.lastHost], ['addresses', BigInt(result.addresses).toLocaleString()], ['usableHosts', BigInt(result.usableHosts).toLocaleString()], + ] : [] + return ( +
+
{ e.preventDefault(); void calculate() }}> + + +

{t('input.hint')}

+
+ {error != null &&

{errorMessage(error)}

} + {!result && !error &&

{t('empty')}

} + {result &&
+ } title={t('result.title')} extra={{result.cidr}} bodyClassName="p-3"> +
+ {fields.map(([key, value]) =>

{t(`result.${key}`)}

{value}
)} +
+

{t(result.version === 4 ? 'result.hostHint' : 'result.ipv6Hint')}

+
+ } title={t('plan.title')} bodyClassName="space-y-3 p-3"> +

{t('plan.hint', { cidr: result.cidr, prefix: result.prefix, max: result.version === 4 ? 32 : 128 })}

+
{ e.preventDefault(); void split() }}> + + +
+ {planError != null &&

{errorMessage(planError)}

} + {plan && <> +
{t('plan.count', { total: BigInt(plan.total).toLocaleString() })}
{t('plan.page', { page: plan.page + 1 })}
+
{['subnet', 'range', 'usableHosts'].map((key) => )}{plan.subnets.map((subnet) => )}
{t(`plan.${key}`)}
{subnet.cidr}
{subnet.firstHost} – {subnet.lastHost}{BigInt(subnet.usableHosts).toLocaleString()}
+ } +
+
} +
+ ) +} diff --git a/plugins/cidr-calculator/ui/tsconfig.json b/plugins/cidr-calculator/ui/tsconfig.json new file mode 100644 index 00000000..625277c6 --- /dev/null +++ b/plugins/cidr-calculator/ui/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6db31ff9..1713a618 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2997,6 +2997,34 @@ importers: specifier: 8.3.0 version: 8.3.0(@types/node@24.13.6)(jiti@2.7.0) + plugins/cidr-calculator/ui: + dependencies: + '@toolforge/plugin-ui-sdk': + specifier: workspace:* + version: link:../../../packages/plugin-ui-sdk + '@toolforge/ui': + specifier: workspace:* + version: link:../../../packages/ui + lucide-react: + specifier: 1.47.0 + version: 1.47.0(react@19.3.0) + react-i18next: + specifier: 17.0.15 + version: 17.0.15(i18next@26.4.2(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) + devDependencies: + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + react: + specifier: 19.3.0 + version: 19.3.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 8.3.0 + version: 8.3.0(@types/node@24.13.6)(jiti@2.7.0) + plugins/context-menu/ui: dependencies: '@toolforge/plugin-ui-sdk':