convert codebase to typescript + introduce biome lint

This commit is contained in:
Sayan Jyoti Das
2026-01-21 23:50:41 +05:30
parent d587d1ac11
commit e95da857d6
82 changed files with 1897 additions and 746 deletions
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"files": {
"ignore": [
"dist",
"node_modules",
"public/pyodide",
"**/*.svelte"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"overrides": [
{
"include": [
"src/modules/wasm/worker/**"
],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
+1 -1
View File
@@ -9,6 +9,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-39
View File
@@ -1,39 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": false,
"baseUrl": ".",
"paths": {
"$lib": ["./src/lib"],
"$lib/*": ["./src/lib/*"],
"$src": ["./src"],
"$src/*": ["./src/*"]
}
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}
+258
View File
@@ -16,6 +16,7 @@
"svelte-spa-router": "^4.0.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@internationalized/date": "^3.8.1",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -25,11 +26,13 @@
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.2",
"typescript": "^5.8.3",
"vite": "^6.4.1"
}
},
@@ -47,6 +50,170 @@
"node": ">=6.0.0"
}
},
"node_modules/@biomejs/biome": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4",
"@biomejs/cli-darwin-x64": "1.9.4",
"@biomejs/cli-linux-arm64": "1.9.4",
"@biomejs/cli-linux-arm64-musl": "1.9.4",
"@biomejs/cli-linux-x64": "1.9.4",
"@biomejs/cli-linux-x64-musl": "1.9.4",
"@biomejs/cli-win32-arm64": "1.9.4",
"@biomejs/cli-win32-x64": "1.9.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz",
@@ -1468,6 +1635,22 @@
"dev": true,
"license": "MIT/X11"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2127,6 +2310,16 @@
"svelte": "^5.7.0"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2201,6 +2394,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexparam": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
@@ -2282,6 +2489,19 @@
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/sass-embedded": {
"version": "1.89.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz",
@@ -2801,6 +3021,30 @@
"node": ">=18"
}
},
"node_modules/svelte-check": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-sonner": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
@@ -3001,6 +3245,20 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+6
View File
@@ -6,11 +6,15 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"check": "tsc -p tsconfig.json --noEmit && svelte-check && biome lint .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"preview": "vite preview",
"deploy": "npm run build && npx wrangler pages deploy dist"
},
"devDependencies": {
"@internationalized/date": "^3.8.1",
"@biomejs/biome": "^1.9.4",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.0",
@@ -19,10 +23,12 @@
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.8.3",
"tw-animate-css": "^1.3.2",
"vite": "^6.4.1"
},
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import Router from 'svelte-spa-router';
import routes from './routes';
</script>
+4
View File
@@ -0,0 +1,4 @@
declare module "*.svelte" {
import type { SvelteComponent } from "svelte";
export default class Component extends SvelteComponent {}
}
@@ -1,11 +1,11 @@
<script>
<script lang="ts">
import * as Menubar from "$lib/components/ui/menubar";
import * as Dialog from "$lib/components/ui/dialog";
import * as IDS from "$src/modules/api/ids.svelte.js";
import * as API from "$src/modules/api/api.svelte.js";
import { error, success, info } from "$src/modules/utils/toast.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte";
import * as API from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
let { isOpen = false } = $props();
let { isOpen = false } : { isOpen?: boolean } = $props();
function openForum() {
window.open('https://community.osarch.org', '_blank');
@@ -23,8 +23,9 @@
try {
await IDS.openDocument();
} catch (err) {
if (err.message !== 'File selection cancelled') {
error('Error opening file: ' + err.message);
const message = err instanceof Error ? err.message : String(err);
if (message !== 'File selection cancelled') {
error(`Error opening file: ${message}`);
console.error(err);
}
}
@@ -50,7 +51,7 @@
success('Audit completed successfully');
} catch (err) {
console.error("Audit failed: ", err);
error(`Audit failed: check console for details`);
error("Audit failed: check console for details");
}
}
</script>
@@ -142,4 +143,4 @@
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
@@ -1,6 +1,5 @@
<script>
import { onMount } from "svelte";
import { Module } from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import { Module } from "$src/modules/api/ids.svelte";
</script>
<div class="app-ribbon">
@@ -20,4 +19,4 @@
<span>Error</span>
</div>
{/if}
</div>
</div>
@@ -1,14 +1,16 @@
<script>
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip";
import { IFCModels, loadIfc, unloadIfc, auditIfc, openIfc, createAuditReport, clearIdsAuditReports, runAudit } from "$src/modules/api/api.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte.js";
import { error, success } from "$src/modules/utils/toast.svelte.js";
import { IFCModels, openIfc, unloadIfc, runAudit } from "$src/modules/api/api.svelte";
import * as IDS from "$src/modules/api/ids.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte.js";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte";
import { onMount } from 'svelte';
import type { AuditReport } from "$src/types/report";
let isAuditing = $state(false);
let activeTab = $state('home');
let activeTab = $state<"home" | "bonsai">('home');
// biome-ignore lint/style/useConst: Svelte state uses assignment for updates.
let isMinimized = $state(false);
const handleLoadModel = async () => {
@@ -16,15 +18,17 @@
await openIfc();
success('IFC model loaded successfully');
} catch (err) {
error(`Failed to load IFC model: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to load IFC model: ${message}`);
}
};
const handleUnloadModel = async (modelId) => {
const handleUnloadModel = async (modelId: string) => {
try {
await unloadIfc(modelId);
} catch (err) {
error(`Failed to unload model: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to unload model: ${message}`);
}
};
@@ -35,14 +39,14 @@
success('Audit completed successfully');
} catch (err) {
console.error("Audit failed: ", err);
error(`Audit failed: check console for details`);
error("Audit failed: check console for details");
} finally {
isAuditing = false;
}
};
const handleViewAuditReport = (auditId) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId);
const handleViewAuditReport = (auditId: string) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId) as AuditReport | undefined;
if (!auditReport) return;
// Switch to the IDS document that was used for this audit
@@ -58,7 +62,7 @@
}
};
const formatFileSize = (bytes) => {
const formatFileSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
@@ -91,7 +95,7 @@
<div class="buttons">
<Tooltip.Provider>
{#if isMinimized}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
<ChevronRightIcon size={24} />
@@ -102,7 +106,7 @@
</Tooltip.Content>
</Tooltip.Root>
{/if}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<button class="tb-btn {activeTab === 'home' ? 'active' : ''}" onclick={() => activeTab = 'home'} aria-label="Home">
<svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@@ -114,7 +118,7 @@
<p>Home</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<button class="tb-btn {activeTab === 'bonsai' ? 'active' : ''}" onclick={() => activeTab = 'bonsai'} aria-label="Bonsai Integration">
<svg style="height: 20px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve">
@@ -692,4 +696,4 @@
opacity: 0.5;
}
}
</style>
</style>
@@ -1,7 +1,9 @@
<script>
<script lang="ts">
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let { addFacet } = $props();
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const { addFacet } : { addFacet: (facetType: FacetType) => void } = $props();
</script>
<DropdownMenu.Root>
@@ -63,4 +65,4 @@
Part Of
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
@@ -1,13 +1,22 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument } from "$src/types/ids";
function switchDocument(docId) {
function switchDocument(docId: string) {
IDS.Module.activeDocument = docId;
}
function closeDocument(docId) {
function closeDocument(docId: string) {
IDS.deleteDocument(docId);
}
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.currentTarget !== event.target) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script>
<div class="ids-tabs">
@@ -15,10 +24,13 @@
<div
class="ids-tab"
class:active={IDS.Module.activeDocument === docId}
role="button"
tabindex="0"
onclick={() => switchDocument(docId)}
aria-label={doc.info.title || "Untitled"}
onkeydown={(event) => handleActivation(event, () => switchDocument(docId))}
aria-label={(doc as IdsDocument).info.title || "Untitled"}
>
<span class="tab-title">{doc.info.title || "Untitled"}</span>
<span class="tab-title">{(doc as IdsDocument).info.title || "Untitled"}</span>
<button
class="tab-close"
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
@@ -31,4 +43,4 @@
</div>
{/each}
<div class="filler-tab"></div>
</div>
</div>
+2 -1
View File
@@ -576,7 +576,8 @@ html, body {
display: flex;
flex-direction: column;
label {
label,
.form-label {
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -1,8 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
import * as Dialog from "./index";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
portalProps?: Record<string, unknown>;
children?: Snippet;
showCloseButton?: boolean;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -11,7 +20,7 @@
children,
showCloseButton = true,
...restProps
} = $props();
} : Props = $props();
</script>
<Dialog.Portal {...portalProps}>
@@ -35,4 +44,4 @@
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
</Dialog.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Description
@@ -14,4 +19,4 @@
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
/>
@@ -1,11 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -15,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Overlay
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Title
@@ -14,4 +19,4 @@
data-slot="dialog-title"
class={cn("text-lg leading-none", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
checked?: boolean;
indeterminate?: boolean;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
checked = $bindable(false),
@@ -10,7 +18,7 @@
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,14 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
sideOffset?: number;
portalProps?: Record<string, unknown>;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
@@ -22,4 +29,4 @@
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
@@ -1,14 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Item
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,20 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
children?: Snippet;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +25,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,11 +1,16 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
value?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
value = $bindable(),
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
@@ -13,4 +18,4 @@
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
/>
@@ -1,14 +1,22 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
value: string;
children?: Snippet<[ { checked: boolean } ]>;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
</DropdownMenuPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<span
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubContent
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,7 +1,15 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -9,7 +17,7 @@
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
@@ -24,4 +32,4 @@
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
</DropdownMenuPrimitive.SubTrigger>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
checked?: boolean;
indeterminate?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
@@ -10,7 +18,7 @@
indeterminate = $bindable(false),
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem>
</MenubarPrimitive.CheckboxItem>
@@ -1,6 +1,19 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Align = "start" | "center" | "end";
type Side = "top" | "bottom" | "left" | "right";
type Props = {
ref?: HTMLElement | null;
class?: string;
sideOffset?: number;
alignOffset?: number;
align?: Align;
side?: Side;
portalProps?: Record<string, unknown>;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -11,7 +24,7 @@
side = "bottom",
portalProps,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Portal {...portalProps}>
@@ -28,4 +41,4 @@
)}
{...restProps}
/>
</MenubarPrimitive.Portal>
</MenubarPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
inset,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,10 +1,14 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let {
ref = $bindable(null),
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
@@ -1,6 +1,13 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -8,7 +15,7 @@
inset = undefined,
variant = "default",
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Item
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
children?: Snippet;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
inset,
children,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +24,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,14 +1,22 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
value: string;
children?: Snippet<[ { checked: boolean } ]>;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</MenubarPrimitive.RadioItem>
</MenubarPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="menubar-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<span
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.SubContent
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,7 +1,15 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -9,7 +17,7 @@
inset = undefined,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.SubTrigger
@@ -24,4 +32,4 @@
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</MenubarPrimitive.SubTrigger>
</MenubarPrimitive.SubTrigger>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Trigger
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Root
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,8 +1,8 @@
<script>
<script lang="ts">
import { Toaster as Sonner } from "svelte-sonner";
import { mode } from "mode-watcher";
let { ...restProps } = $props();
let { ...restProps } : Record<string, unknown> = $props();
</script>
<Sonner
@@ -10,4 +10,4 @@
class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps}
/>
/>
@@ -1,6 +1,18 @@
<script>
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Side = "top" | "bottom" | "left" | "right";
type Props = {
ref?: HTMLElement | null;
class?: string;
sideOffset?: number;
side?: Side;
children?: Snippet;
arrowClasses?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -10,7 +22,7 @@
children,
arrowClasses,
...restProps
} = $props();
} : Props = $props();
</script>
<TooltipPrimitive.Portal>
@@ -42,4 +54,4 @@
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Portal>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
-8
View File
@@ -1,8 +0,0 @@
import { clsx, } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+7
View File
@@ -0,0 +1,7 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-9
View File
@@ -1,9 +0,0 @@
import { mount } from 'svelte';
import './css/app.css';
import App from './App.svelte';
const app = mount(App, {
target: document.getElementById('root'),
});
export default app;
+14
View File
@@ -0,0 +1,14 @@
import { mount } from 'svelte';
import './css/app.css';
import App from './App.svelte';
const root = document.getElementById('root');
if (!root) {
throw new Error('Missing root element');
}
const app = mount(App, {
target: root,
});
export default app;
@@ -1,8 +1,31 @@
import wasm from "$src/modules/wasm";
import * as IDS from "$src/modules/api/ids.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte";
import hyperid from "hyperid";
import type { AuditReport, AuditReportData } from "$src/types/report";
import type { IdsDocument } from "$src/types/ids";
export let Autocompletions = $state({
type AutocompletionState = {
entityClasses: string[];
materialCategories: string[];
classificationSystems: Record<string, { source: string; tokens: string[] | null }>;
dataTypes: string[];
isLoaded: boolean;
};
type IfcModel = {
id: string;
fileName: string;
fileSize: number;
loadedAt: Date;
};
type IfcModelState = {
models: IfcModel[];
isLoading: boolean;
audits: AuditReport[];
};
export const Autocompletions: AutocompletionState = $state({
entityClasses: [],
materialCategories: [],
classificationSystems: {},
@@ -10,13 +33,13 @@ export let Autocompletions = $state({
isLoaded: false
});
export let IFCModels = $state({
export const IFCModels: IfcModelState = $state({
models: [],
isLoading: false,
audits: []
});
const id = hyperid();
const id: () => string = hyperid();
// Preload autocompletions on initialization
wasm.init().then(async () => {
@@ -30,26 +53,30 @@ export async function preloadAutocompletions() {
// Entity classes
const entitySets = await Promise.all(
schemas.map(schema => wasm.getAllEntityClasses(schema))
);
const allEntities = new Set();
entitySets.forEach(entities => {
entities.forEach(entity => allEntities.add(entity.toUpperCase()));
});
) as string[][];
const allEntities = new Set<string>();
for (const entities of entitySets) {
for (const entity of entities) {
allEntities.add(entity.toUpperCase());
}
}
// Data types
const dataTypeSets = await Promise.all(
schemas.map(schema => wasm.getAllDataTypes(schema))
);
const allDataTypes = new Set();
dataTypeSets.forEach(dataTypes => {
Object.keys(dataTypes).forEach(dataType => allDataTypes.add(dataType));
});
) as Record<string, string>[];
const allDataTypes = new Set<string>();
for (const dataTypes of dataTypeSets) {
for (const dataType of Object.keys(dataTypes as Record<string, string>)) {
allDataTypes.add(dataType);
}
}
// Material categories and Classification systems
const [materialCategories, classificationSystems] = await Promise.all([
wasm.getMaterialCategories(),
wasm.getStandardClassificationSystems()
]);
]) as [string[], AutocompletionState["classificationSystems"]];
// Cache autocompletions
Autocompletions.entityClasses = Array.from(allEntities).sort();
@@ -64,15 +91,15 @@ export async function preloadAutocompletions() {
}
}
export async function getPredefinedTypes(schema, entity) {
export async function getPredefinedTypes(schema: string, entity: string) {
return await wasm.getPredefinedTypes(schema, entity);
}
export async function getEntityAttributes(schema, entity) {
export async function getEntityAttributes(schema: string, entity: string) {
return await wasm.getEntityAttributes(schema, entity);
}
export async function getApplicablePsets(schema, entity, predefinedType = '') {
export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') {
return await wasm.getApplicablePsets(schema, entity, predefinedType);
}
@@ -92,7 +119,7 @@ export function getDataTypes() {
return Autocompletions.dataTypes;
}
export async function loadIfc(file) {
export async function loadIfc(file: File): Promise<IfcModel> {
try {
IFCModels.isLoading = true;
@@ -100,10 +127,10 @@ export async function loadIfc(file) {
const uint8Array = new Uint8Array(arrayBuffer);
// Load IFC model
const ifcId = await wasm.loadIfc(Array.from(uint8Array));
const ifcId = await wasm.loadIfc(Array.from(uint8Array)) as string;
// Add to models list
const model = {
const model: IfcModel = {
id: ifcId,
fileName: file.name,
fileSize: file.size,
@@ -121,7 +148,7 @@ export async function loadIfc(file) {
}
}
export async function unloadIfc(modelId) {
export async function unloadIfc(modelId: string) {
try {
// Unload model
await wasm.unloadIfc(modelId);
@@ -136,9 +163,9 @@ export async function unloadIfc(modelId) {
}
}
export async function auditIfc(modelId, idsData) {
export async function auditIfc(modelId: string, idsData: string | Uint8Array | ArrayBuffer) {
try {
let idsBytes;
let idsBytes: Uint8Array;
if (typeof idsData === 'string') {
idsBytes = new TextEncoder().encode(idsData);
} else if (idsData instanceof ArrayBuffer) {
@@ -148,7 +175,7 @@ export async function auditIfc(modelId, idsData) {
}
// Run audit
const auditResult = await wasm.auditIfc(modelId, idsBytes);
const auditResult = await wasm.auditIfc(modelId, idsBytes) as { json: AuditReportData; html: string };
console.log(`Audit completed for model ${modelId}`);
return auditResult;
@@ -163,13 +190,14 @@ export function getLoadedModels() {
}
export async function openIfc() {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.ifc';
fileInput.onchange = async (event) => {
const file = event.target.files[0];
const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
@@ -194,15 +222,20 @@ export async function openIfc() {
});
}
export function getIfcById(modelId) {
export function getIfcById(modelId: string) {
return IFCModels.models.find(model => model.id === modelId);
}
export function createAuditReport(modelId, document, auditData, htmlReport = null) {
export function createAuditReport(
modelId: string,
document: string,
auditData: AuditReportData,
htmlReport: string | null = null
): AuditReport | undefined {
const model = getIfcById(modelId);
if (!model) return;
const auditReport = {
const auditReport: AuditReport = {
id: id(),
modelId: modelId,
modelName: model.fileName,
@@ -216,19 +249,19 @@ export function createAuditReport(modelId, document, auditData, htmlReport = nul
return auditReport;
}
export function getAuditReportsForIfc(modelId) {
export function getAuditReportsForIfc(modelId: string) {
return IFCModels.audits.filter(audit => audit.modelId === modelId);
}
export function getAuditReportById(auditId) {
export function getAuditReportById(auditId: string) {
return IFCModels.audits.find(audit => audit.id === auditId);
}
export function clearIdsAuditReports(document) {
export function clearIdsAuditReports(document: string) {
IFCModels.audits = IFCModels.audits.filter(audit => audit.document !== document);
}
export async function downloadAuditReport(auditId) {
export async function downloadAuditReport(auditId: string) {
const audit = getAuditReportById(auditId);
if (!audit || !audit.htmlReport) {
throw new Error('HTML report not available for this audit');
@@ -237,7 +270,7 @@ export async function downloadAuditReport(auditId) {
// Get IDS document title for filename
let filename = 'report.html';
if (audit.document && IDS.Module.documents[audit.document]) {
const doc = IDS.Module.documents[audit.document];
const doc = IDS.Module.documents[audit.document] as IdsDocument;
const title = doc.info?.title || 'untitled';
filename = `report_${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`;
}
@@ -270,9 +303,12 @@ export async function runAudit() {
// Get the active IDS document XML
const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
// Run audit on all loaded models
let firstAuditReport = null;
let firstAuditReport: AuditReport | undefined;
for (const model of IFCModels.models) {
const result = await auditIfc(model.id, idsXml);
@@ -280,7 +316,10 @@ export async function runAudit() {
const jsonData = result.json || null;
const htmlReport = result.html || null;
const auditReport = createAuditReport(model.id, IDS.Module.activeDocument, jsonData, htmlReport);
if (!jsonData) {
continue;
}
const auditReport = createAuditReport(model.id, IDS.Module.activeDocument as string, jsonData, htmlReport);
// Store the first audit report to open in viewer
if (!firstAuditReport) {
@@ -1,12 +1,37 @@
import { io } from 'socket.io-client';
import { IFCModels } from './api.svelte.js';
import * as IDS from './ids.svelte.js';
import { error, success } from '../utils/toast.svelte.js';
import type { Socket } from 'socket.io-client';
import { IFCModels } from './api.svelte';
import * as IDS from './ids.svelte';
import { error, success } from '../utils/toast.svelte';
import hyperid from 'hyperid';
import { onMount } from 'svelte';
import type { AuditReport, AuditReportData } from "$src/types/report";
// Bonsai connection state
export let Bonsai = $state({
type BonsaiState = {
enabled: boolean;
port: string | null;
socket: Socket | null;
connected: boolean;
auditing: boolean;
};
type PendingAudit = {
resolve: (value: string | null) => void;
reject: (reason?: unknown) => void;
};
type AuditResultPayload = {
id?: string;
json_report?: string;
html_report?: string;
};
type AuditErrorPayload = {
id?: string;
error?: string;
};
export const Bonsai: BonsaiState = $state({
enabled: false,
port: null,
socket: null,
@@ -14,8 +39,8 @@ export let Bonsai = $state({
auditing: false
});
const id = hyperid();
const pendingAudits = new Map();
const id: () => string = hyperid();
const pendingAudits = new Map<string, PendingAudit>();
// Check for Bonsai server port in URL parameters
const urlParams = new URLSearchParams(window.location.search);
@@ -29,8 +54,11 @@ if (serverPort) {
/**
* Connect to Bonsai server
*/
export const connect = () => new Promise((resolve, reject) => {
if (!Bonsai.port) return;
export const connect = () => new Promise<void>((resolve, reject) => {
if (!Bonsai.port) {
resolve();
return;
}
try {
Bonsai.socket = io(`ws://127.0.0.1:${Bonsai.port}/ifctester`, {
@@ -49,7 +77,7 @@ export const connect = () => new Promise((resolve, reject) => {
Bonsai.connected = false;
});
Bonsai.socket.on('connect_error', (err) => {
Bonsai.socket.on('connect_error', (err: Error) => {
Bonsai.connected = false;
error(`Failed to connect to Bonsai: ${err.message}`);
reject(err);
@@ -59,7 +87,8 @@ export const connect = () => new Promise((resolve, reject) => {
Bonsai.socket.on('error', handleAuditError);
} catch (err) {
error(`Failed to connect to Bonsai: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to connect to Bonsai: ${message}`);
reject(err);
}
});
@@ -93,14 +122,21 @@ export const runAudit = async () => {
// Convert IDS document to XML string
const idsXml = await IDS.exportActiveDocument();
if (!idsXml) {
throw new Error('Failed to export IDS document');
}
const requestId = id();
const socket = Bonsai.socket;
if (!socket) {
throw new Error('Bonsai socket not connected');
}
return new Promise((resolve, reject) => {
return new Promise<string | null>((resolve, reject) => {
// Store request with resolve/reject functions
pendingAudits.set(requestId, { resolve, reject });
Bonsai.socket.emit('audit_ids', {
socket.emit('audit_ids', {
id: requestId,
ids: idsXml
});
@@ -108,7 +144,8 @@ export const runAudit = async () => {
} catch (err) {
Bonsai.auditing = false;
error(`Failed to run Bonsai audit: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to run Bonsai audit: ${message}`);
return null;
}
};
@@ -117,7 +154,7 @@ export const runAudit = async () => {
* Handles audit results from Bonsai server
* @param {Object} data - Audit result data
*/
const handleAuditResult = (data) => {
const handleAuditResult = (data: AuditResultPayload) => {
if (!data.id || !data.json_report) return;
const pendingAudit = pendingAudits.get(data.id);
@@ -130,13 +167,14 @@ const handleAuditResult = (data) => {
const { resolve } = pendingAudit;
try {
const reportData = JSON.parse(data.json_report);
const reportData = JSON.parse(data.json_report) as AuditReportData;
const auditReport = {
const auditReport: AuditReport = {
id: data.id,
modelId: `bonsai:${data.id}`,
date: new Date().toISOString(),
modelName: 'Bonsai IFC Model',
document: IDS.Module.activeDocument,
document: IDS.Module.activeDocument ?? "",
data: reportData,
htmlReport: data.html_report
};
@@ -152,7 +190,8 @@ const handleAuditResult = (data) => {
} catch (err) {
Bonsai.auditing = false;
error(`Failed to process audit result: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to process audit result: ${message}`);
resolve(null);
}
};
@@ -161,7 +200,7 @@ const handleAuditResult = (data) => {
* Handles audit errors from Bonsai server
* @param {Object} data - Error data
*/
const handleAuditError = (data) => {
const handleAuditError = (data: AuditErrorPayload) => {
if (!data.id) return;
const pendingAudit = pendingAudits.get(data.id);
@@ -174,7 +213,6 @@ const handleAuditError = (data) => {
const { resolve } = pendingAudit;
Bonsai.auditing = false;
error(`Audit failed (Bonsai): ${data.error}`);
error(`Audit failed (Bonsai): ${data.error ?? "Unknown error"}`);
resolve(null);
};
@@ -1,10 +1,18 @@
import wasm from "$src/modules/wasm";
import { clearIdsAuditReports } from "./api.svelte.js";
import { clearIdsAuditReports } from "./api.svelte";
import hyperid from "hyperid";
import {tick} from "svelte";
import type { DocumentState, Facet, FacetValue, IdsDocument, IdsCardinality, Restriction, Specification } from "$src/types/ids";
export let Module = $state({
documents: [],
type ModuleState = {
documents: Record<string, IdsDocument>;
activeDocument: string | null;
status: "loading" | "ready" | "error";
states: Record<string, DocumentState>;
};
export const Module: ModuleState = $state({
documents: {},
activeDocument: null,
status: "loading",
states: {}
@@ -17,14 +25,15 @@ wasm.init().then(() => {
Module.status = "error";
});
const id = hyperid()
const id: () => string = hyperid();
export function setDocumentState(docId, updates) {
export function setDocumentState(docId: string, updates: Partial<DocumentState>) {
if (!Module.states[docId]) {
Module.states[docId] = {
activeTab: 'info',
viewMode: 'editor',
activeSpecification: null
activeSpecification: null,
auditReport: null
};
}
Object.assign(Module.states[docId], updates);
@@ -32,7 +41,7 @@ export function setDocumentState(docId, updates) {
export async function createDocument() {
const docId = id();
const doc = await wasm.createIDS();
const doc = await wasm.createIDS() as IdsDocument;
Module.documents[docId] = doc;
@@ -43,14 +52,14 @@ export async function createDocument() {
Module.activeDocument = docId;
}
export async function deleteDocument(id) {
export async function deleteDocument(id: string) {
// Clear any audit reports generated using this IDS document
clearIdsAuditReports(id);
delete Module.documents[id];
delete Module.states[id];
if (Module.activeDocument == id) {
if (Module.activeDocument === id) {
// If there are other documents, set the first one as active
if (Object.keys(Module.documents).length > 0) {
Module.activeDocument = Object.keys(Module.documents)[0];
@@ -62,19 +71,19 @@ export async function deleteDocument(id) {
// Normalize (remove xs: prefix) from JSON dict returned from Python
// We need this because the backend exports with xs: prefix, yet expects a dict without prefixes.
function normalizeIdsDict(obj) {
function normalizeIdsDict(obj: unknown): unknown {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(normalizeIdsDict);
}
const result = {};
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (key === 'xs:restriction' && Array.isArray(value) && value.length > 0) {
// Convert xs:restriction array to restriction object
const restriction = value[0];
const newRestriction = {};
const restriction = value[0] as Record<string, unknown>;
const newRestriction: Record<string, unknown> = {};
for (const [restrictionKey, restrictionValue] of Object.entries(restriction)) {
if (restrictionKey.startsWith('xs:')) {
@@ -86,7 +95,7 @@ function normalizeIdsDict(obj) {
}
}
result['restriction'] = newRestriction;
result.restriction = newRestriction;
} else {
result[key] = normalizeIdsDict(value);
}
@@ -96,13 +105,16 @@ function normalizeIdsDict(obj) {
}
export async function openDocument() {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
return new Promise<void>((resolve, reject) => {
const fileInput = document.createElement('input') as HTMLInputElement & {
oncancel?: ((this: HTMLInputElement, ev: Event) => void) | null;
};
fileInput.type = 'file';
fileInput.accept = '.ids,.xml';
fileInput.onchange = async (event) => {
const file = event.target.files[0];
const target = event.target as HTMLInputElement | null;
const file = target?.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
@@ -112,8 +124,8 @@ export async function openDocument() {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const fileContent = e.target.result;
const doc = normalizeIdsDict(await wasm.openIDS(fileContent, false));
const fileContent = (e.target as FileReader).result;
const doc = normalizeIdsDict(await wasm.openIDS(String(fileContent), false)) as IdsDocument;
const docId = id();
// Add document to list and set as active
@@ -145,16 +157,16 @@ export async function openDocument() {
});
}
export async function exportActiveDocument() {
export async function exportActiveDocument(): Promise<string | null> {
if (!Module.activeDocument) return null;
const doc = $state.snapshot(Module.documents[Module.activeDocument]);
const xmlString = await wasm.exportIDS(doc);
const xmlString = await wasm.exportIDS(doc as Record<string, unknown>) as string;
return xmlString;
}
export async function exportDocument(docId) {
export async function exportDocument(docId: string) {
const doc = $state.snapshot(Module.documents[docId]);
// Validate
@@ -162,37 +174,38 @@ export async function exportDocument(docId) {
throw new Error("Please create at least one specification before exporting the document.");
}
const xmlString = await wasm.exportIDS(doc);
const xmlString = await wasm.exportIDS(doc as Record<string, unknown>) as string;
// Create and download file
const blob = new Blob([xmlString], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${Module.documents[docId].info.title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`;
const title = Module.documents[docId].info.title || "untitled";
a.download = `${title.replace(/[^a-zA-Z0-9]/g, '_')}.ids`;
a.click();
URL.revokeObjectURL(url);
}
export async function createSpecification(docId) {
const spec = await wasm.createSpecification();
export async function createSpecification(docId: string) {
const spec = await wasm.createSpecification() as Specification;
// Add specification to document
Module.documents[docId].specifications.specification.push(spec);
// Set as active specification
if (Module.activeDocument == docId) {
if (Module.activeDocument === docId) {
const state = Module.states[docId];
state.activeSpecification = Module.documents[docId].specifications.specification.length - 1;
}
}
export async function deleteSpecification(docId, specId) {
export async function deleteSpecification(docId: string, specId: number) {
Module.documents[docId].specifications.specification.splice(specId, 1);
if (Module.activeDocument == docId) {
if (Module.activeDocument === docId) {
const state = Module.states[docId];
if (state.activeSpecification == specId) {
if (state.activeSpecification === specId) {
// We need to wait for the next tick here because of Svelte's internal shenanigans
await tick();
setDocumentState(docId, { activeSpecification: null });
@@ -209,37 +222,56 @@ export async function deleteSpecification(docId, specId) {
* clause: "applicability", "requirements"
* facet: "entity", "attribute", "classification", "partOf", "property", "material"
*/
export async function createFacet(docId, specId, clause, facet) {
let facetObj;
if (facet == "entity") {
facetObj = await wasm.createEntityFacet(clause, {});
} else if (facet == "attribute") {
facetObj = await wasm.createAttributeFacet(clause, {});
} else if (facet == "classification") {
facetObj = await wasm.createClassificationFacet(clause, {});
} else if (facet == "partOf") {
facetObj = await wasm.createPartOfFacet(clause, {});
} else if (facet == "property") {
facetObj = await wasm.createPropertyFacet(clause, {});
} else if (facet == "material") {
facetObj = await wasm.createMaterialFacet(clause, {});
export async function createFacet(
docId: string,
specId: number,
clause: "applicability" | "requirements",
facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material"
) {
let facetObj: Facet | undefined;
if (facet === "entity") {
facetObj = await wasm.createEntityFacet(clause, {}) as Facet;
} else if (facet === "attribute") {
facetObj = await wasm.createAttributeFacet(clause, {}) as Facet;
} else if (facet === "classification") {
facetObj = await wasm.createClassificationFacet(clause, {}) as Facet;
} else if (facet === "partOf") {
facetObj = await wasm.createPartOfFacet(clause, {}) as Facet;
} else if (facet === "property") {
facetObj = await wasm.createPropertyFacet(clause, {}) as Facet;
} else if (facet === "material") {
facetObj = await wasm.createMaterialFacet(clause, {}) as Facet;
}
if (!(facet in Module.documents[docId].specifications.specification[specId][clause])) {
Module.documents[docId].specifications.specification[specId][clause][facet] = [];
if (!facetObj) return;
const spec = Module.documents[docId].specifications.specification[specId];
const clauseKey = clause as "applicability" | "requirements";
if (!spec[clauseKey]) spec[clauseKey] = {};
if (!(facet in (spec[clauseKey] as Record<string, unknown>))) {
(spec[clauseKey] as Record<string, unknown>)[facet] = [];
}
Module.documents[docId].specifications.specification[specId][clause][facet].push(facetObj);
((spec[clauseKey] as Record<string, unknown>)[facet] as Facet[]).push(facetObj);
}
export async function deleteFacet(docId, specId, clause, facet, facetId) {
delete Module.documents[docId].specifications.specification[specId][clause][facet][facetId];
export async function deleteFacet(
docId: string,
specId: number,
clause: "applicability" | "requirements",
facet: "entity" | "attribute" | "classification" | "partOf" | "property" | "material",
facetId: number
) {
const spec = Module.documents[docId].specifications.specification[specId];
const list = (spec[clause] as Record<string, unknown> | undefined)?.[facet] as Facet[] | undefined;
if (!list) return;
delete list[facetId];
}
export function getSpecUsage(spec) {
export function getSpecUsage(spec?: Specification | null): IdsCardinality {
if (!spec?.applicability) return 'required';
const minOccurs = spec.applicability["@minOccurs"];
const maxOccurs = spec.applicability["@maxOccurs"];
const minOccurs = spec.applicability["@minOccurs"] as number | undefined;
const maxOccurs = spec.applicability["@maxOccurs"] as number | "unbounded" | undefined;
if (minOccurs === 1 && maxOccurs === "unbounded") return 'required';
if (minOccurs === 0 && maxOccurs === "unbounded") return 'optional';
@@ -248,85 +280,90 @@ export function getSpecUsage(spec) {
};
// Converts facet to human-readable description
export function stringifyFacet(clauseType, facet, facetType, spec) {
export function stringifyFacet(
clauseType: "applicability" | "requirements",
facet: Facet,
facetType: string,
spec?: Specification | null
) {
if (!facet) return "";
const usage = getSpecUsage(spec);
const descriptions = [];
const descriptions: string[] = [];
// Entity facet
if (facetType === "entity") {
if (clauseType === "applicability") {
descriptions.push(`All data where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`All data where IFC class ${stringifyValue(facet.name as FacetValue)}`);
} else {
descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`Shall be data where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
if (facet.predefinedType) {
descriptions.push(`and type ${stringifyValue(facet.predefinedType)}`);
descriptions.push(`and type ${stringifyValue(facet.predefinedType as FacetValue)}`);
}
}
// Attribute facet
else if (facetType === "attribute") {
if (clauseType === "applicability") {
descriptions.push(`All data where attribute ${stringifyValue(facet.name)}`);
descriptions.push(`All data where attribute ${stringifyValue(facet.name as FacetValue)}`);
} else {
descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name)}`);
descriptions.push(`Shall be data where attribute ${stringifyValue(facet.name as FacetValue)}`);
}
descriptions.push(`and value ${stringifyValue(facet.value)}`);
descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`);
}
// Property facet
else if (facetType === "property") {
if (clauseType === "applicability") {
descriptions.push(`Elements where property ${stringifyValue(facet.baseName)}`);
descriptions.push(`Elements where property ${stringifyValue(facet.baseName as FacetValue)}`);
} else {
descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName)}`);
descriptions.push(`Shall be elements where property ${stringifyValue(facet.baseName as FacetValue)}`);
}
if (facet.value) {
descriptions.push(`and value ${stringifyValue(facet.value)}`);
descriptions.push(`and value ${stringifyValue(facet.value as FacetValue)}`);
}
descriptions.push(`and dataset ${stringifyValue(facet.propertySet)}`);
descriptions.push(`and dataset ${stringifyValue(facet.propertySet as FacetValue)}`);
}
// Classification facet
else if (facetType === "classification") {
if (clauseType === "applicability") {
descriptions.push(`All data where classification system ${stringifyValue(facet.system)}`);
descriptions.push(`All data where classification system ${stringifyValue(facet.system as FacetValue)}`);
} else {
descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system)}`);
descriptions.push(`Shall be data where classification system ${stringifyValue(facet.system as FacetValue)}`);
}
if (facet.value) {
descriptions.push(`and classification ${stringifyValue(facet.value)}`);
descriptions.push(`and classification ${stringifyValue(facet.value as FacetValue)}`);
}
}
// Material facet
else if (facetType === "material") {
if (clauseType === "applicability") {
descriptions.push(`All data where material ${stringifyValue(facet.value)}`);
descriptions.push(`All data where material ${stringifyValue(facet.value as FacetValue)}`);
} else {
descriptions.push(`Shall be data where material ${stringifyValue(facet.value)}`);
descriptions.push(`Shall be data where material ${stringifyValue(facet.value as FacetValue)}`);
}
}
// PartOf facet
else if (facetType === "partOf") {
if (clauseType === "applicability") {
descriptions.push(`An element with an **${facet['@relation']}** relationship`);
descriptions.push(`An element with an **${String(facet['@relation'] ?? "")}** relationship`);
if (facet.name) {
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
} else {
descriptions.push(`An element shall have an **${facet['@relation']}** relationship`);
descriptions.push(`An element shall have an **${String(facet['@relation'] ?? "")}** relationship`);
if (facet.name) {
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name)}`);
descriptions.push(`with an entity where IFC class ${stringifyValue(facet.name as FacetValue)}`);
}
if (facet.predefinedType) {
descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType)}`);
descriptions.push(`and predefined type ${stringifyValue(facet.predefinedType as FacetValue)}`);
}
}
}
@@ -336,20 +373,22 @@ export function stringifyFacet(clauseType, facet, facetType, spec) {
// Post-process for prohibited and optional requirements
let isProhibited = false;
if (usage == "prohibited") isProhibited = !isProhibited;
if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "prohibited") isProhibited = !isProhibited;
if (usage === "prohibited") isProhibited = !isProhibited;
if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "prohibited") {
isProhibited = !isProhibited;
}
if (isProhibited)
combined = combined.replace("Shall", "Shall not").replace("shall", "shall not");
if (clauseType == "requirements" && "@cardinality" in facet && facet["@cardinality"] == "optional")
if (clauseType === "requirements" && "@cardinality" in facet && facet["@cardinality"] === "optional")
combined = combined.replace("Shall", "May").replace("shall", "may");
return renderFacetString(combined);
}
// Converts value objects to human-readable strings
function stringifyValue(value) {
function stringifyValue(value?: FacetValue) {
if (!value) return "is provided";
if (value.simpleValue) return `is **${value.simpleValue}**`;
if (value.restriction) return stringifyRestriction(value.restriction);
@@ -357,7 +396,7 @@ function stringifyValue(value) {
}
// Converts restriction objects to human-readable strings
function stringifyRestriction(restriction) {
function stringifyRestriction(restriction: Restriction) {
if (!restriction) return "";
// Handle enumeration
@@ -394,7 +433,7 @@ function stringifyRestriction(restriction) {
if (restriction.maxExclusive && restriction.maxExclusive.length > 0) {
parts.push(`**< ${restriction.maxExclusive[0]['@value'] || ''}**`);
}
return parts.length > 0 ? "is in range " + parts.join(", ") : "has range restriction";
return parts.length > 0 ? `is in range ${parts.join(", ")}` : "has range restriction";
}
// Handle length range restrictions
@@ -406,18 +445,18 @@ function stringifyRestriction(restriction) {
if (restriction.maxLength && restriction.maxLength.length > 0) {
parts.push(`**max length ${restriction.maxLength[0]['@value'] || ''}**`);
}
return parts.length > 0 ? "has " + parts.join(", ") : "has length range restriction";
return parts.length > 0 ? `has ${parts.join(", ")}` : "has length range restriction";
}
return "has complex restriction";
}
function renderFacetString(text) {
function renderFacetString(text: string): string {
// Convert **text** to <strong>text</strong>
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
const withStrong = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Convert `text` to <code>text</code>
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
const withCode = withStrong.replace(/`([^`]+)`/g, '<code>$1</code>');
return text;
}
return withCode;
}
@@ -4,7 +4,7 @@ import { toast } from "svelte-sonner";
* Show an error toast notification
* @param {string} message - The error message to display
*/
export function error(message) {
export function error(message: string): void {
toast.error(message);
}
@@ -12,7 +12,7 @@ export function error(message) {
* Show a success toast notification
* @param {string} message - The success message to display
*/
export function success(message) {
export function success(message: string): void {
toast.success(message);
}
@@ -20,7 +20,7 @@ export function success(message) {
* Show an info toast notification
* @param {string} message - The info message to display
*/
export function info(message) {
export function info(message: string): void {
toast.info(message);
}
@@ -28,7 +28,7 @@ export function info(message) {
* Show a warning toast notification
* @param {string} message - The warning message to display
*/
export function warning(message) {
export function warning(message: string): void {
toast.warning(message);
}
@@ -37,7 +37,7 @@ export function warning(message) {
* @param {string} message - The loading message to display
* @returns {string} - Toast ID for dismissing later
*/
export function loading(message) {
export function loading(message: string): string | number {
return toast.loading(message);
}
@@ -45,7 +45,7 @@ export function loading(message) {
* Dismiss a specific toast
* @param {string} toastId - The toast ID to dismiss
*/
export function dismiss(toastId) {
export function dismiss(toastId: string | number): void {
toast.dismiss(toastId);
}
@@ -57,10 +57,16 @@ export function dismiss(toastId) {
* @param {string} messages.success - Success message
* @param {string} messages.error - Error message
*/
export function promise(promiseToTrack, messages) {
type PromiseToastMessages = {
loading: string;
success: string;
error: string;
};
export function promise<T>(promiseToTrack: Promise<T>, messages: PromiseToastMessages) {
return toast.promise(promiseToTrack, {
loading: messages.loading,
success: messages.success,
error: messages.error,
});
}
}
@@ -5,6 +5,7 @@
import hyperid from "hyperid";
import EventEmitter from "eventemitter3";
import type { WorkerResponse } from "$src/types/wasm";
// Message types
export const MessageType = {
@@ -25,52 +26,57 @@ export const MessageType = {
// WASM module disposed
DISPOSED: 'disposed'
} as const;
type PendingMessage = {
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
};
type WasmReadyState = boolean | Promise<boolean>;
class WASMModule extends EventEmitter {
id = hyperid();
ready = false;
worker = null;
pendingMessages = new Map();
ready: WasmReadyState = false;
worker: Worker | null = null;
pendingMessages = new Map<string, PendingMessage>();
async init() {
if (this.ready === true) return;
else if (this.ready instanceof Promise) return this.ready;
if (this.ready instanceof Promise) return this.ready;
this.worker = new Worker(new URL('./worker/worker.js', import.meta.url), {type: 'module'});
this.worker = new Worker(new URL('./worker/worker.ts', import.meta.url), {type: 'module'});
this.worker.onmessage = (event) => {
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
this._handleWorkerMessage(event.data);
};
this.worker.onerror = (error) => {
this.worker.onerror = (error: ErrorEvent) => {
console.error('[WASM] Web worker error:', error);
this._rejectPendingMessages(error);
};
this.ready = new Promise(async (resolve, reject) => {
try {
await this._sendMessage(MessageType.INIT);
resolve(true);
} catch (error) {
this.ready = this._sendMessage(MessageType.INIT)
.then(() => true)
.catch((error) => {
console.error('[WASM] Failed to initialize:', error);
this.ready = false;
reject(error);
}
});
throw error;
});
return this.ready;
}
async _sendMessage(type, payload = {}) {
if (!this.worker) throw new Error('Worker not initialized');
async _sendMessage(type: string, payload: Record<string, unknown> = {}): Promise<unknown> {
const worker = this.worker;
if (!worker) throw new Error('Worker not initialized');
const id = this.id();
return new Promise((resolve, reject) => {
this.pendingMessages.set(id, { resolve, reject });
this.worker.postMessage({
worker.postMessage({
type,
payload,
id
@@ -78,7 +84,7 @@ class WASMModule extends EventEmitter {
});
}
_handleWorkerMessage({ type, payload, id }) {
_handleWorkerMessage({ type, payload, id }: WorkerResponse) {
const pendingMessage = this.pendingMessages.get(id);
if (!pendingMessage) {
@@ -97,23 +103,28 @@ class WASMModule extends EventEmitter {
case MessageType.API_RESPONSE:
resolve(payload);
break;
case MessageType.ERROR:
reject(new Error(payload.message));
case MessageType.ERROR: {
const message =
payload && typeof payload === "object" && "message" in payload
? String(payload.message)
: "Unknown worker error";
reject(new Error(message));
break;
}
default:
console.warn('[WASM] Unknown message type:', type);
reject(new Error(`Unknown message type: ${type}`));
}
}
_rejectPendingMessages(error) {
_rejectPendingMessages(error: unknown) {
for (const { reject } of this.pendingMessages.values()) {
reject(error);
}
this.pendingMessages.clear();
}
async _apiCall(method, ...args) {
async _apiCall(method: string, ...args: unknown[]) {
if (!this.ready) await this.init();
const result = await this._sendMessage(MessageType.API_CALL, { method, args });
@@ -123,35 +134,35 @@ class WASMModule extends EventEmitter {
/**
* Get all entity classes in a given IFC schema
*/
async getAllEntityClasses(schema) {
async getAllEntityClasses(schema: string) {
return this._apiCall('getAllEntityClasses', schema);
}
/**
* Get all data types in a given IFC schema
*/
async getAllDataTypes(schema) {
async getAllDataTypes(schema: string) {
return this._apiCall('getAllDataTypes', schema);
}
/**
* Get predefined types for a given IFC entity
*/
async getPredefinedTypes(schema, entity) {
async getPredefinedTypes(schema: string, entity: string) {
return this._apiCall('getPredefinedTypes', schema, entity);
}
/**
* Get all attributes for a given IFC entity
*/
async getEntityAttributes(schema, entity) {
async getEntityAttributes(schema: string, entity: string) {
return this._apiCall('getEntityAttributes', schema, entity);
}
/**
* Get applicable property sets for a given IFC entity
*/
async getApplicablePsets(schema, entity, predefinedType = '') {
async getApplicablePsets(schema: string, entity: string, predefinedType = '') {
return this._apiCall('getApplicablePsets', schema, entity, predefinedType);
}
@@ -172,21 +183,21 @@ class WASMModule extends EventEmitter {
/**
* Load an IFC file. Returns a unique ID for the loaded file.
*/
async loadIfc(ifcData) {
async loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) {
return this._apiCall('loadIfc', ifcData);
}
/**
* Unload an IFC file
*/
async unloadIfc(ifcId) {
async unloadIfc(ifcId: string) {
return this._apiCall('unloadIfc', ifcId);
}
/**
* Audit a loaded IFC file against IDS specifications
*/
async auditIfc(ifcId, idsData) {
async auditIfc(ifcId: string, idsData: ArrayBuffer | Uint8Array | number[]) {
const idsBytes = idsData instanceof ArrayBuffer ? new Uint8Array(idsData) : idsData;
return this._apiCall('auditIfc', ifcId, Array.from(idsBytes));
@@ -204,70 +215,70 @@ class WASMModule extends EventEmitter {
/**
* Open an existing IDS from XML string
*/
async openIDS(idsXml, validate = false) {
async openIDS(idsXml: string, validate = false) {
return this._apiCall('openIDS', idsXml, validate);
}
/**
* Create a specification
*/
async createSpecification(options = {}) {
async createSpecification(options: Record<string, unknown> = {}) {
return this._apiCall('createSpecification', options);
}
/**
* Create an entity facet
*/
async createEntityFacet(clause, options = {}) {
async createEntityFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createEntityFacet', clause, options);
}
/**
* Create an attribute facet
*/
async createAttributeFacet(clause, options = {}) {
async createAttributeFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createAttributeFacet', clause, options);
}
/**
* Create a property facet
*/
async createPropertyFacet(clause, options = {}) {
async createPropertyFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPropertyFacet', clause, options);
}
/**
* Create a material facet
*/
async createMaterialFacet(clause, options = {}) {
async createMaterialFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createMaterialFacet', clause, options);
}
/**
* Create a classification facet
*/
async createClassificationFacet(clause, options = {}) {
async createClassificationFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createClassificationFacet', clause, options);
}
/**
* Create a part-of facet
*/
async createPartOfFacet(clause, options = {}) {
async createPartOfFacet(clause: string, options: Record<string, unknown> = {}) {
return this._apiCall('createPartOfFacet', clause, options);
}
/**
* Validate an IDS object
*/
async validateIDS(idsObj) {
async validateIDS(idsObj: Record<string, unknown>) {
return await this._apiCall('validateIDS', idsObj);
}
/**
* Export IDS instance to XML string
*/
async exportIDS(idsObj) {
async exportIDS(idsObj: Record<string, unknown>) {
return this._apiCall('exportIDS', idsObj);
}
@@ -319,4 +330,4 @@ export const {
dispose
} = wasm;
export default wasm;
export default wasm;
@@ -1,12 +1,13 @@
import config from '../../../config.json';
import hyperid from 'hyperid';
import type { AuditReportData } from "$src/types/report";
let pyodide = null;
let id = hyperid();
let pyodide: any = null;
const id = hyperid();
let LoadedIFC = new Map();
const LoadedIFC = new Map<string, unknown>();
export async function init(pdide) {
export async function init(pdide: any) {
pyodide = pdide;
// Load Python API bindings
@@ -18,7 +19,7 @@ export async function init(pdide) {
`);
}
export async function getPredefinedTypes(schema, entity) {
export async function getPredefinedTypes(schema: string, entity: string) {
const result = await pyodide.runPythonAsync(`
from api import get_predefined_types_for_entity
predef_types = get_predefined_types_for_entity("${schema}", "${entity}")
@@ -27,7 +28,7 @@ export async function getPredefinedTypes(schema, entity) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getAllEntityClasses(schema) {
export async function getAllEntityClasses(schema: string) {
const result = await pyodide.runPythonAsync(`
from api import get_all_entity_classes
entities = get_all_entity_classes("${schema}")
@@ -36,7 +37,7 @@ export async function getAllEntityClasses(schema) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getAllDataTypes(schema) {
export async function getAllDataTypes(schema: string) {
const result = await pyodide.runPythonAsync(`
from api import get_all_data_types
data_types = get_all_data_types("${schema}")
@@ -45,7 +46,7 @@ export async function getAllDataTypes(schema) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getEntityAttributes(schema, entity) {
export async function getEntityAttributes(schema: string, entity: string) {
const result = await pyodide.runPythonAsync(`
from api import get_entity_attributes
attrs = get_entity_attributes("${schema}", "${entity}")
@@ -54,7 +55,7 @@ export async function getEntityAttributes(schema, entity) {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function getApplicablePsets(schema, entity, predefinedType = '') {
export async function getApplicablePsets(schema: string, entity: string, predefinedType = '') {
const result = await pyodide.runPythonAsync(`
from api import get_applicable_psets
psets = get_applicable_psets("${schema}", "${entity}", "${predefinedType}")
@@ -81,7 +82,7 @@ export async function getStandardClassificationSystems() {
return result.toJs({ dict_converter: Object.fromEntries });
}
export async function loadIfc(ifcData) {
export async function loadIfc(ifcData: number[] | Uint8Array | ArrayBuffer) {
const ifc_id = id();
const path = `/tmp/${encodeURIComponent(ifc_id)}.ifc`;
@@ -97,14 +98,14 @@ export async function loadIfc(ifcData) {
return ifc_id;
}
export async function unloadIfc(ifcId) {
export async function unloadIfc(ifcId: string) {
const path = `/tmp/${encodeURIComponent(ifcId)}.ifc`;
pyodide.FS.unlink(path);
LoadedIFC.delete(ifcId);
}
export async function auditIfc(ifcId, idsData) {
export async function auditIfc(ifcId: string, idsData: number[] | Uint8Array | ArrayBuffer) {
const reporter = pyodide.pyimport("ifctester.reporter");
const api = pyodide.pyimport("api");
@@ -116,16 +117,16 @@ export async function auditIfc(ifcId, idsData) {
specs.validate(ifc);
// Create report in both HTML and JSON formats
let jsonReporter = reporter.Json(specs);
const jsonReporter = reporter.Json(specs);
jsonReporter.report();
const jsonReport = jsonReporter.to_string();
let htmlReporter = reporter.Html(specs);
const htmlReporter = reporter.Html(specs);
htmlReporter.report();
const htmlReport = htmlReporter.to_string();
return {
json: JSON.parse(jsonReport),
json: JSON.parse(jsonReport) as AuditReportData,
html: htmlReport
};
}
@@ -142,4 +143,4 @@ export const API = {
"loadIfc": loadIfc,
"unloadIfc": unloadIfc,
"auditIfc": auditIfc
};
};
@@ -2,13 +2,19 @@
* IDS module
*/
let pyodide = null;
let pyodide: any = null;
// IDS Python classes
let Ids, Specification;
let Entity, Attribute, Property, Material, Classification, PartOf;
let Ids: any;
let Specification: any;
let Entity: any;
let Attribute: any;
let Property: any;
let Material: any;
let Classification: any;
let PartOf: any;
export async function init(pdide) {
export async function init(pdide: any) {
pyodide = pdide;
await pyodide.loadPackagesFromImports(`
@@ -29,24 +35,24 @@ export async function init(pdide) {
PartOf = pyodide.pyimport("ifctester.facet").PartOf;
}
function _idsToInstance(idsObj) {
function _idsToInstance(idsObj: Record<string, unknown>) {
const ids_raw = Ids();
return ids_raw.parse(pyodide.toPy(idsObj))
}
export function createIDS() {
export function createIDS(): Record<string, unknown> {
const ids_raw = Ids()
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
}
export function openIDS(ids_xml, validate = false) {
export function openIDS(ids_xml: string, validate = false): Record<string, unknown> {
const ids_from_xml_string = pyodide.pyimport("api").ids_from_xml_string;
const ids_raw = ids_from_xml_string(ids_xml, validate);
return ids_raw.asdict().toJs({dict_converter: Object.fromEntries});
}
export function validateIDS(idsObj) {
export function validateIDS(idsObj: Record<string, unknown>): boolean {
const ids_raw = _idsToInstance(idsObj)
const tempFilename = `temp_${Date.now()}.xml`;
const isValid = ids_raw.to_xml(tempFilename); // to_xml validates the XML as well, as far as I understand
@@ -60,12 +66,26 @@ export function validateIDS(idsObj) {
return isValid;
}
export function exportIDS(idsObj) {
export function exportIDS(idsObj: Record<string, unknown>): string {
const ids_raw = _idsToInstance(idsObj)
return ids_raw.to_string();
}
export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "IFC4"], identifier = null, description = null, instructions = null, usage = "required"}) {
export function createSpecification({
name = "Unnamed",
ifcVersion = ["IFC2X3", "IFC4"],
identifier = null,
description = null,
instructions = null,
usage = "required"
}: {
name?: string;
ifcVersion?: string[];
identifier?: string | null;
description?: string | null;
instructions?: string | null;
usage?: string;
} = {}): Record<string, unknown> {
const spec = Specification.callKwargs({
name: name,
ifcVersion: ifcVersion,
@@ -79,7 +99,14 @@ export function createSpecification({name = "Unnamed", ifcVersion = ["IFC2X3", "
}
// @instructions
export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = null, instructions = null}) {
export function createEntityFacet(
clause: string,
{name = "IFCWALL", predefinedType = null, instructions = null}: {
name?: string;
predefinedType?: string | null;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const entity = Entity.callKwargs({
name: name,
predefinedType: predefinedType,
@@ -89,7 +116,15 @@ export function createEntityFacet(clause, {name = "IFCWALL", predefinedType = nu
}
// @cardinality, @instructions
export function createAttributeFacet(clause, {name = "Name", value = null, cardinality = "required", instructions = null}) {
export function createAttributeFacet(
clause: string,
{name = "Name", value = null, cardinality = "required", instructions = null}: {
name?: string;
value?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const attribute = Attribute.callKwargs({
name: name,
value: value,
@@ -100,7 +135,16 @@ export function createAttributeFacet(clause, {name = "Name", value = null, cardi
}
// @uri, @cardinality, @instructions
export function createClassificationFacet(clause, {value = null, system = null, uri = null, cardinality = "required", instructions = null}) {
export function createClassificationFacet(
clause: string,
{value = null, system = null, uri = null, cardinality = "required", instructions = null}: {
value?: string | null;
system?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const classification = Classification.callKwargs({
value: value,
system: system,
@@ -112,7 +156,16 @@ export function createClassificationFacet(clause, {value = null, system = null,
}
// @relation, @cardinality, @instructions
export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}) {
export function createPartOfFacet(
clause: string,
{name = "IFCWALL", predefinedType = null, relation = null, cardinality = "required", instructions = null}: {
name?: string;
predefinedType?: string | null;
relation?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const part_of = PartOf.callKwargs({
name: name,
predefinedType: predefinedType,
@@ -124,7 +177,26 @@ export function createPartOfFacet(clause, {name = "IFCWALL", predefinedType = nu
}
// @dataType, @uri, @cardinality, @instructions
export function createPropertyFacet(clause, {propertySet = "Property_Set", baseName = "propertyName", value = null, dataType = null, uri = null, cardinality = "required", instructions = null}) {
export function createPropertyFacet(
clause: string,
{
propertySet = "Property_Set",
baseName = "propertyName",
value = null,
dataType = null,
uri = null,
cardinality = "required",
instructions = null
}: {
propertySet?: string;
baseName?: string;
value?: string | null;
dataType?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const property = Property.callKwargs({
propertySet: propertySet,
baseName: baseName,
@@ -138,7 +210,15 @@ export function createPropertyFacet(clause, {propertySet = "Property_Set", baseN
}
// @uri, @cardinality, @instructions
export function createMaterialFacet(clause, {value = null, uri = null, cardinality = "required", instructions = null}) {
export function createMaterialFacet(
clause: string,
{value = null, uri = null, cardinality = "required", instructions = null}: {
value?: string | null;
uri?: string | null;
cardinality?: string;
instructions?: string | null;
} = {}
): Record<string, unknown> {
const material = Material.callKwargs({
value: value,
uri: uri,
@@ -149,7 +229,7 @@ export function createMaterialFacet(clause, {value = null, uri = null, cardinali
}
// Helper function to convert date to ISO format string
export function formatDate(date) {
export function formatDate(date?: string | number | Date | null): string | null {
if (!date) return null;
const d = new Date(date);
return d.toISOString().split('T')[0];
@@ -168,4 +248,4 @@ export const API = {
"createPartOfFacet": createPartOfFacet,
"createPropertyFacet": createPropertyFacet,
"createMaterialFacet": createMaterialFacet,
};
};
@@ -4,13 +4,14 @@
import { MessageType } from '../index';
import config from '../../../config.json';
import * as IDS from './ids.js';
import * as IDS from './ids';
import * as API from './api';
import type { ApiCallPayload, WorkerRequest } from "$src/types/wasm";
let pyodide = null;
let pyodide: any = null;
let ready = false;
self.addEventListener('message', async (event) => {
self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
console.log("[worker] Received message:", event.data);
const { type, payload, id } = event.data;
@@ -25,27 +26,33 @@ self.addEventListener('message', async (event) => {
});
break;
case MessageType.API_CALL:
case MessageType.API_CALL: {
if (!ready) {
throw new Error('[worker] Pyodide not initialized');
}
const result = await handleApiCall(payload);
if (!payload) {
throw new Error('[worker] Missing payload for API call');
}
const result = await handleApiCall(payload as ApiCallPayload);
self.postMessage({
type: MessageType.API_RESPONSE,
payload: result,
id
});
break;
}
default:
throw new Error(`[worker] Unknown message type: ${type}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
self.postMessage({
type: MessageType.ERROR,
payload: {
message: error.message,
stack: error.stack
message,
stack
},
id
});
@@ -93,17 +100,17 @@ async function cleanupEnvironment() {
console.log("[worker] Closed environment");
}
async function handleApiCall({ method, args = [] }) {
async function handleApiCall({ method, args = [] }: ApiCallPayload) {
if (method === 'internal.cleanup') {
await cleanupEnvironment();
return true;
}
if (method in API.API) {
return await API.API[method](...args);
} else if (method in IDS.API) {
return await IDS.API[method](...args);
} else {
throw new Error(`[worker] Unknown API method: ${method}`);
return await (API.API as Record<string, (...params: unknown[]) => unknown>)[method](...args);
}
}
if (method in IDS.API) {
return await (IDS.API as Record<string, (...params: unknown[]) => unknown>)[method](...args);
}
throw new Error(`[worker] Unknown API method: ${method}`);
}
@@ -1,37 +1,56 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.svelte';
import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte";
import type { DocumentState, Facet, IdsDocument, Specification } from "$src/types/ids";
let { activeTab } = $props();
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
async function addFacet (facetType) {
if (!activeSpecification) return;
async function addFacet(facetType: FacetType) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"applicability",
facetType
);
}
async function removeFacet(facetType, facetIndex) {
if (!activeSpecification) return;
async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"applicability",
facetType,
facetIndex
);
}
let applicabilityEntries = $derived(
activeSpecification?.applicability
? (Object.entries(activeSpecification.applicability).filter(
([facetType, facets]) =>
facetType !== "@minOccurs" &&
facetType !== "@maxOccurs" &&
Array.isArray(facets)
) as Array<[FacetType, Facet[]]>)
: []
);
</script>
<div class="restrictions-panel">
@@ -40,22 +59,19 @@
<CreateFacetDropdown {addFacet} />
</div>
<div class="restrictions-list">
{#if activeSpecification?.applicability}
{#each Object.entries(activeSpecification.applicability) as [facetType, facets]}
{#if facetType !== "@minOccurs" && facetType !== "@maxOccurs"}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
{facetType}
specification={activeSpecification}
activeTab="applicability"
{removeFacet}
{index}
key={`${activeDocument}-${documentState?.activeSpecification}-applicability-${facetType}-${index}`}
/>
{/each}
{/if}
{#if activeSpecification && applicabilityEntries.length > 0}
{#each applicabilityEntries as [facetType, facets]}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
{facetType}
specification={activeSpecification}
activeTab="applicability"
{removeFacet}
{index}
/>
{/each}
{/each}
{/if}
</div>
</div>
</div>
@@ -1,20 +1,40 @@
<script>
<script lang="ts">
import RestrictionEditor from './RestrictionEditor.svelte';
import {stringifyFacet} from "$src/modules/api/ids.svelte.js";
import {stringifyFacet} from "$src/modules/api/ids.svelte";
import type { Facet, Specification } from "$src/types/ids";
/**
* Applicability facets wont have: "@uri", "@instructions", "@cardinality"
* @ in name --> simple string value
* else --> can be simpleValue, Restriction or list of Restrictions
*/
let { facet = $bindable(), facetType, activeTab, removeFacet, index, specification } = $props();
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const getSpecialProp = (prop) => {
return facet[prop] ?? "";
let {
facet = $bindable<Facet>({}),
facetType,
activeTab,
removeFacet,
index,
specification
}: {
facet: Facet;
facetType: FacetType;
activeTab: "applicability" | "requirements";
removeFacet: (facetType: FacetType, facetIndex: number) => void | Promise<void>;
index: number;
specification: Specification;
} = $props();
const getSpecialProp = (prop: string) => {
const value = (facet as Record<string, unknown>)[prop];
return typeof value === "string" ? value : "";
};
const setSpecialProp = (prop, value) => {
facet[prop] = value;
const setSpecialProp = (prop: string, value: string) => {
(facet as Record<string, unknown>)[prop] = value;
};
const baseId = `facet-${facetType}-${index}`;
</script>
<div class="restriction-item">
@@ -48,8 +68,8 @@
<RestrictionEditor bind:facet={facet} fieldName="name" label="Entity Name" placeholder="e.g., IfcSpace" autocomplete="entityName" />
<RestrictionEditor bind:facet={facet} fieldName="predefinedType" label="Predefined Type" placeholder="e.g., SOLIDWALL" autocomplete="predefinedType" />
<div class="form-group">
<label>Relation</label>
<select class="form-input" bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}>
<label for={`${baseId}-relation`}>Relation</label>
<select class="form-input" id={`${baseId}-relation`} bind:value={() => getSpecialProp("@relation"), (v) => setSpecialProp("@relation", v)}>
<option value="">Select relation...</option>
<option value="IFCRELAGGREGATES">IFCRELAGGREGATES</option>
<option value="IFCRELASSIGNSTOGROUP">IFCRELASSIGNSTOGROUP</option>
@@ -62,8 +82,8 @@
{#if activeTab === 'requirements'}
{#if facetType !== 'entity'}
<div class="form-group">
<label>Cardinality</label>
<select class="form-input" bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}>
<label for={`${baseId}-cardinality`}>Cardinality</label>
<select class="form-input" id={`${baseId}-cardinality`} bind:value={() => getSpecialProp("@cardinality"), (v) => setSpecialProp("@cardinality", v)}>
<option value="required">Required</option>
<option value="optional">Optional</option>
<option value="prohibited">Prohibited</option>
@@ -71,9 +91,9 @@
</div>
{/if}
<div class="form-group full-width">
<label>Instructions</label>
<textarea class="form-input" bind:value={() => getSpecialProp("@instructions"), (v) => setSpecialProp("@instructions", v)} placeholder="Optional instructions for IFC authors" rows="2"></textarea>
<label for={`${baseId}-instructions`}>Instructions</label>
<textarea class="form-input" id={`${baseId}-instructions`} bind:value={() => getSpecialProp("@instructions"), (v) => setSpecialProp("@instructions", v)} placeholder="Optional instructions for IFC authors" rows="2"></textarea>
</div>
{/if}
</div>
</div>
</div>
@@ -1,13 +1,17 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument, IdsInfo } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
const getProp = (prop) => {
const getProp = (prop: keyof IdsInfo) => {
return activeDocument?.info[prop] ?? "";
};
const setProp = (prop, value) => {
const setProp = (prop: keyof IdsInfo, value: string) => {
if (!activeDocument) return;
activeDocument.info[prop] = value;
};
</script>
@@ -18,36 +22,36 @@
</div>
<div class="form-grid">
<div class="form-group">
<label>Title</label>
<input class="form-input" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
<label for="ids-title">Title</label>
<input class="form-input" id="ids-title" type="text" bind:value={() => getProp("title"), (v) => setProp("title", v)} placeholder="Enter IDS title">
</div>
<div class="form-group">
<label>Author Email</label>
<input class="form-input" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
<label for="ids-author">Author Email</label>
<input class="form-input" id="ids-author" type="email" bind:value={() => getProp("author"), (v) => setProp("author", v)} placeholder="Enter author">
</div>
<div class="form-group">
<label>Version</label>
<input class="form-input" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
<label for="ids-version">Version</label>
<input class="form-input" id="ids-version" type="text" bind:value={() => getProp("version"), (v) => setProp("version", v)} placeholder="Enter version">
</div>
<div class="form-group">
<label>Date</label>
<input class="form-input" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}>
<label for="ids-date">Date</label>
<input class="form-input" id="ids-date" type="date" bind:value={() => getProp("date"), (v) => setProp("date", v)}>
</div>
<div class="form-group full-width">
<label>Description</label>
<textarea class="form-input" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea>
<label for="ids-description">Description</label>
<textarea class="form-input" id="ids-description" bind:value={() => getProp("description"), (v) => setProp("description", v)} placeholder="Enter description" rows="3"></textarea>
</div>
<div class="form-group">
<label>Purpose</label>
<input class="form-input" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
<label for="ids-purpose">Purpose</label>
<input class="form-input" id="ids-purpose" type="text" bind:value={() => getProp("purpose"), (v) => setProp("purpose", v)} placeholder="Enter purpose">
</div>
<div class="form-group">
<label>Milestone</label>
<input class="form-input" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
<label for="ids-milestone">Milestone</label>
<input class="form-input" id="ids-milestone" type="text" bind:value={() => getProp("milestone"), (v) => setProp("milestone", v)} placeholder="Enter milestone">
</div>
<div class="form-group full-width">
<label>Copyright</label>
<input class="form-input" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
<label for="ids-copyright">Copyright</label>
<input class="form-input" id="ids-copyright" type="text" bind:value={() => getProp("copyright"), (v) => setProp("copyright", v)} placeholder="Enter copyright">
</div>
</div>
</div>
@@ -1,18 +1,26 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte.js";
import { error, success } from "$src/modules/utils/toast.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import { getAuditReportById, downloadAuditReport } from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import * as Tooltip from "$src/lib/components/ui/tooltip";
import type { AuditReport, AuditReportData } from "$src/types/report";
import type { DocumentState, IdsDocument } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let auditReport = $derived(documentState?.auditReport ? getAuditReportById(documentState.auditReport) : null);
let expandedSpecs = $state(new Set());
let expandedRequirements = $state(new Set());
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let auditReport = $derived(
documentState?.auditReport ? (getAuditReportById(documentState.auditReport) as AuditReport | undefined) : null
);
let expandedSpecs = $state(new Set<number>());
let expandedRequirements = $state(new Set<string>());
let allExpanded = $state(false);
// Open Editor mode and jump to a specific specification
function editSpecification(index) {
function editSpecification(index: number) {
if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, {
viewMode: 'editor',
@@ -23,13 +31,13 @@
}
}
function toggleSpecification(index) {
function toggleSpecification(index: number) {
if (expandedSpecs.has(index)) {
expandedSpecs.delete(index);
} else {
expandedSpecs.add(index);
}
expandedSpecs = new Set(expandedSpecs);
expandedSpecs = new Set<number>(expandedSpecs);
}
function toggleAllSpecifications() {
@@ -37,7 +45,7 @@
if (allExpanded) {
// Collapse all
expandedSpecs = new Set();
expandedSpecs = new Set<number>();
allExpanded = false;
} else {
// Expand all
@@ -47,26 +55,26 @@
}
}
function getSpecificationStatus(specIndex, auditData) {
function getSpecificationStatus(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
return spec.is_skipped ? 'skipped' : spec.status;
}
function getSpecificationStats(specIndex, auditData) {
function getSpecificationStats(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
return {
requirements: `${spec.total_requirements || 0}`,
requirementsPassed: `${spec.total_requirements_pass || 0}`,
checksTotal: `${spec.total_checks || 0}`,
checksPassed: `${spec.total_checks_pass || 0}`,
applicableTotal: `${spec.total_applicable || 0}`,
applicablePassed: `${spec.total_applicable_pass || 0}`
requirements: spec.total_requirements || 0,
requirementsPassed: spec.total_requirements_pass || 0,
checksTotal: spec.total_checks || 0,
checksPassed: spec.total_checks_pass || 0,
applicableTotal: spec.total_applicable || 0,
applicablePassed: spec.total_applicable_pass || 0
};
}
function getSpecificationReason(specIndex, auditData) {
function getSpecificationReason(specIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec) return null;
@@ -97,29 +105,37 @@
await downloadAuditReport(auditReport.id);
success('Audit report downloaded successfully');
} catch (err) {
error(`Failed to download report: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to download report: ${message}`);
}
}
function getRequirementStatus(specIndex, reqIndex, auditData) {
function getRequirementStatus(specIndex: number, reqIndex: number, auditData: AuditReportData) {
const spec = auditData.specifications[specIndex];
if (!spec || !spec.requirements || !spec.requirements[reqIndex]) return null;
return spec.requirements[reqIndex];
}
function toggleRequirementDetails(specIndex, reqIndex) {
function toggleRequirementDetails(specIndex: number, reqIndex: number) {
const key = `${specIndex}-${reqIndex}`;
if (expandedRequirements.has(key)) {
expandedRequirements.delete(key);
} else {
expandedRequirements.add(key);
}
expandedRequirements = new Set(expandedRequirements);
expandedRequirements = new Set<string>(expandedRequirements);
}
function isRequirementDetailsExpanded(specIndex, reqIndex) {
function isRequirementDetailsExpanded(specIndex: number, reqIndex: number) {
return expandedRequirements.has(`${specIndex}-${reqIndex}`);
}
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script>
<div class="ids-viewer">
@@ -208,7 +224,14 @@
</div>
{#each activeDocument.specifications.specification as spec, index}
<div class="specification-card {auditReport ? 'with-audit' : ''} {auditReport && getSpecificationStatus(index, auditReport.data) !== null ? (getSpecificationStatus(index, auditReport.data) === 'skipped' ? 'spec-skipped' : (getSpecificationStatus(index, auditReport.data) ? 'spec-pass' : 'spec-fail')) : ''}">
<div class="spec-card-header" onclick={() => toggleSpecification(index)}>
<div
class="spec-card-header"
role="button"
tabindex="0"
aria-expanded={expandedSpecs.has(index)}
onclick={() => toggleSpecification(index)}
onkeydown={(event) => handleActivation(event, () => toggleSpecification(index))}
>
<div class="spec-title-section">
<div class="spec-title-row">
<h2>{spec["@name"] || `Specification ${index + 1}`}</h2>
@@ -427,7 +450,7 @@
{/if}
{/if}
</button>
{#if isRequirementDetailsExpanded(index, facetIndex)}
{#if reqAuditData && isRequirementDetailsExpanded(index, facetIndex)}
<div class="facet-expansion">
<div class="entity-tables">
{#if reqAuditData.passed_entities && reqAuditData.passed_entities.length > 0}
@@ -1,37 +1,53 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import FacetEditor from './FacetEditor.svelte';
import CreateFacetDropdown from "$src/components/CreateFacetDropdown.svelte";
import type { DocumentState, Facet, IdsDocument, Specification } from "$src/types/ids";
let { activeTab } = $props();
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
async function addFacet (facetType) {
if (!activeSpecification) return;
async function addFacet(facetType: FacetType) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.createFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"requirements",
facetType
);
}
async function removeFacet(facetType, facetIndex) {
if (!activeSpecification) return;
async function removeFacet(facetType: FacetType, facetIndex: number) {
if (!activeSpecification || !documentState || !IDS.Module.activeDocument) return;
await IDS.deleteFacet(
IDS.Module.activeDocument,
documentState.activeSpecification,
documentState.activeSpecification ?? 0,
"requirements",
facetType,
facetIndex
);
}
let requirementEntries = $derived(
activeSpecification?.requirements
? (Object.entries(activeSpecification.requirements).filter(([, facets]) =>
Array.isArray(facets)
) as Array<[FacetType, Facet[]]>)
: []
);
</script>
<div class="restrictions-panel">
@@ -40,8 +56,8 @@
<CreateFacetDropdown {addFacet} />
</div>
<div class="restrictions-list">
{#if activeSpecification?.requirements}
{#each Object.entries(activeSpecification.requirements) as [facetType, facets]}
{#if activeSpecification && requirementEntries.length > 0}
{#each requirementEntries as [facetType, facets]}
{#each facets as facet, index}
<FacetEditor
bind:facet={facets[index]}
@@ -50,10 +66,9 @@
activeTab="requirements"
{removeFacet}
{index}
key={`${activeDocument}-${documentState?.activeSpecification}-requirements-${facetType}-${index}`}
/>
{/each}
{/each}
{/if}
</div>
</div>
</div>
@@ -1,9 +1,34 @@
<script>
<script lang="ts">
import Svelecte from 'svelecte';
import { getEntityClasses, getMaterialCategories, getClassificationSystems, getDataTypes, getPredefinedTypes, getEntityAttributes, getApplicablePsets } from '$src/modules/api/api.svelte.js';
import * as IDS from '$src/modules/api/ids.svelte.js';
import { getEntityClasses, getMaterialCategories, getClassificationSystems, getDataTypes, getPredefinedTypes, getEntityAttributes, getApplicablePsets } from '$src/modules/api/api.svelte';
import * as IDS from '$src/modules/api/ids.svelte';
import type { DocumentState, Facet, FacetValue, IdsDocument, Restriction, RestrictionValue, Specification } from '$src/types/ids';
let { facet = $bindable(), fieldName, label, placeholder, autocomplete = null, isSpecialProp = false } = $props();
type AutocompleteType =
| 'entityName'
| 'material'
| 'classificationSystem'
| 'predefinedType'
| 'attributeName'
| 'propertySet'
| 'dataType'
| null;
let {
facet = $bindable<Facet>({}),
fieldName,
label,
placeholder,
autocomplete = null,
isSpecialProp = false
}: {
facet: Facet;
fieldName: string;
label: string;
placeholder: string;
autocomplete?: AutocompleteType;
isSpecialProp?: boolean;
} = $props();
const isEntityNameField = autocomplete === 'entityName';
const isMaterialField = autocomplete === 'material';
@@ -12,22 +37,30 @@
const isAttributeNameField = autocomplete === 'attributeName';
const isPropertySetField = autocomplete === 'propertySet';
const isDataTypeField = autocomplete === 'dataType';
const baseId = `restriction-${fieldName}-${Math.random().toString(36).slice(2, 8)}`;
// Predefined Types autocompletions
let predefinedTypeOptions = $state([]);
let predefinedTypeOptions: string[] = $state([]);
// Attribute Names autocompletions
let attributeNameOptions = $state([]);
let attributeNameOptions: string[] = $state([]);
// Property Sets autocompletions
let propertySetOptions = $state([]);
let propertySetOptions: string[] = $state([]);
// Get the active specification's IFC schemas
const getIfcVersions = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
const activeDocument = IDS.Module.activeDocument
? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument)
: null;
const documentState = IDS.Module.activeDocument
? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState)
: null;
const activeSpecification =
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null;
const versions = activeSpecification?.["@ifcVersion"] || ['IFC4'];
// TODO: Fix: Filter out IFC4X3 because it's buggy
@@ -36,7 +69,7 @@
// Get the entity name from facet
const getEntityName = () => {
const nameField = facet?.name;
const nameField = (facet as Record<string, unknown>)?.name as FacetValue | undefined;
if (!nameField) return '';
if (nameField.simpleValue) return nameField.simpleValue;
return '';
@@ -44,18 +77,24 @@
// Get all entity names from Entity facets in Applicability
const getApplicabilityEntityNames = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
const activeDocument = IDS.Module.activeDocument
? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument)
: null;
const documentState = IDS.Module.activeDocument
? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState)
: null;
const activeSpecification =
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null;
if (!activeSpecification?.applicability?.entity) return [];
const entityFacets = activeSpecification.applicability.entity;
const entityNames = [];
const entityFacets = activeSpecification.applicability.entity as Facet[];
const entityNames: string[] = [];
entityFacets.forEach(entityFacet => {
const nameField = entityFacet.name;
const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
if (!nameField) return;
if (nameField.simpleValue) {
@@ -74,19 +113,25 @@
// Get entity facets with their predefined types
const getApplicabilityEntityFacets = () => {
const activeDocument = IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null;
const documentState = IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null;
const activeSpecification = activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null;
const activeDocument = IDS.Module.activeDocument
? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument)
: null;
const documentState = IDS.Module.activeDocument
? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState)
: null;
const activeSpecification =
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null;
if (!activeSpecification?.applicability?.entity) return [];
return activeSpecification.applicability.entity.map(entityFacet => {
const nameField = entityFacet.name;
const predefinedTypeField = entityFacet.predefinedType;
return (activeSpecification.applicability.entity as Facet[]).map(entityFacet => {
const nameField = (entityFacet as Record<string, unknown>).name as FacetValue | undefined;
const predefinedTypeField = (entityFacet as Record<string, unknown>).predefinedType as FacetValue | undefined;
// Extract entity name
let entityNames = [];
let entityNames: string[] = [];
if (nameField?.simpleValue) {
entityNames = [nameField.simpleValue];
} else if (nameField?.restriction?.enumeration) {
@@ -99,9 +144,12 @@
let predefinedType = '';
if (predefinedTypeField?.simpleValue) {
predefinedType = predefinedTypeField.simpleValue;
} else if (predefinedTypeField?.restriction?.enumeration?.length > 0) {
// For enumeration predefined types, use the first one or empty string
predefinedType = predefinedTypeField.restriction.enumeration[0]['@value'] || '';
} else {
const enumValues = predefinedTypeField?.restriction?.enumeration;
if (enumValues && enumValues.length > 0) {
// For enumeration predefined types, use the first one or empty string
predefinedType = enumValues[0]['@value'] || '';
}
}
return { entityNames, predefinedType };
@@ -109,7 +157,8 @@
};
// Contextual autocompletions
$effect(async () => {
$effect(() => {
void (async () => {
// Predefined Types
if (isPredefinedTypeField) {
@@ -122,10 +171,10 @@
const typePromises = ifcVersions.map(schema =>
getPredefinedTypes(schema, entityName.toUpperCase())
);
const typeSets = await Promise.all(typePromises);
const typeSets = await Promise.all(typePromises) as string[][];
// Deduplicate predefined types across all schemas
const allTypes = new Set();
const allTypes = new Set<string>();
typeSets.forEach(types => {
if (types && Array.isArray(types)) {
types.forEach(type => allTypes.add(type));
@@ -150,19 +199,19 @@
if (entityNames.length > 0 && ifcVersions.length > 0) {
try {
// Fetch attributes for all entity names across all selected schemas
const attributePromises = [];
const attributePromises: Array<Promise<{ name: string }[]>> = [];
entityNames.forEach(entityName => {
ifcVersions.forEach(schema => {
attributePromises.push(
getEntityAttributes(schema, entityName.toUpperCase())
getEntityAttributes(schema, entityName.toUpperCase()) as Promise<{ name: string }[]>
);
});
});
const attributeSets = await Promise.all(attributePromises);
const attributeSets = await Promise.all(attributePromises) as { name: string }[][];
// Deduplicate attribute names across all entities and schemas
const allAttributes = new Set();
const allAttributes = new Set<string>();
attributeSets.forEach(attributes => {
if (attributes && Array.isArray(attributes)) {
attributes.forEach(attr => {
@@ -191,21 +240,21 @@
if (entityFacets.length > 0 && ifcVersions.length > 0) {
try {
// Fetch applicable property sets for all entity facets across all selected schemas
const psetPromises = [];
const psetPromises: Array<Promise<string[]>> = [];
entityFacets.forEach(facet => {
facet.entityNames.forEach(entityName => {
ifcVersions.forEach(schema => {
psetPromises.push(
getApplicablePsets(schema, entityName.toUpperCase(), facet.predefinedType)
getApplicablePsets(schema, entityName.toUpperCase(), facet.predefinedType) as Promise<string[]>
);
});
});
});
const psetSets = await Promise.all(psetPromises);
const psetSets = await Promise.all(psetPromises) as string[][];
// Deduplicate property set names
const allPsets = new Set();
const allPsets = new Set<string>();
psetSets.forEach(psets => {
if (psets && Array.isArray(psets)) {
psets.forEach(pset => allPsets.add(pset));
@@ -221,10 +270,11 @@
propertySetOptions = [];
}
}
})();
});
// Get autocomplete options based on field type
const getAutocompleteOptions = () => {
const getAutocompleteOptions = (): string[] => {
if (isEntityNameField) return getEntityClasses();
if (isMaterialField) return getMaterialCategories();
if (isClassificationSystemField) return Object.keys(getClassificationSystems());
@@ -236,11 +286,11 @@
};
const getRestrictionType = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue) return 'Simple';
if (fieldValue.simpleValue !== undefined) return 'Simple';
if (fieldValue['restriction']) {
const restriction = fieldValue['restriction'];
const restriction = fieldValue['restriction'] as Restriction;
if (restriction['enumeration']) return 'Enumeration';
if (restriction['pattern']) return 'Pattern';
if (restriction['minInclusive'] || restriction['maxInclusive'] ||
@@ -251,39 +301,40 @@
return 'Simple';
};
const getSimpleValue = () => {
const fieldValue = facet[fieldName];
const getSimpleValue = (): string => {
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | string | undefined;
// For special properties (eg. @dataType), we return the value directly
if (isSpecialProp) return fieldValue || null;
if (typeof fieldValue === 'string') return fieldValue;
if (isSpecialProp) return '';
if (!fieldValue) return null;
if (!fieldValue) return '';
if (fieldValue.simpleValue !== undefined) return fieldValue.simpleValue;
return null;
return '';
};
const getEnumerationValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue?.['restriction']) return [''];
const restriction = fieldValue['restriction'];
const enumValues = restriction['enumeration'];
const restriction = fieldValue['restriction'] as Restriction;
const enumValues = restriction['enumeration'] as RestrictionValue[] | undefined;
if (!enumValues) return [''];
return enumValues.map(item => item['@value'] || '');
};
const getPatternValue = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction'];
const pattern = restriction['pattern'];
const restriction = fieldValue['restriction'] as Restriction;
const pattern = restriction['pattern'] as RestrictionValue[] | undefined;
if (!pattern || !pattern.length) return '';
return pattern[0]['@value'] || '';
};
const getRangeValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue?.['restriction']) return { min: '', max: '', minType: 'Inclusive', maxType: 'Inclusive' };
const restriction = fieldValue['restriction'];
const restriction = fieldValue['restriction'] as Restriction;
let min = '', max = '', minType = 'Inclusive', maxType = 'Inclusive';
@@ -307,18 +358,18 @@
};
const getLengthValue = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue?.['restriction']) return '';
const restriction = fieldValue['restriction'];
const length = restriction['length'];
const restriction = fieldValue['restriction'] as Restriction;
const length = restriction['length'] as RestrictionValue[] | undefined;
if (!length || !length.length) return '';
return length[0]['@value'] || '';
};
const getLengthRangeValues = () => {
const fieldValue = facet[fieldName];
const fieldValue = (facet as Record<string, unknown>)[fieldName] as FacetValue | undefined;
if (!fieldValue?.['restriction']) return { min: '', max: '' };
const restriction = fieldValue['restriction'];
const restriction = fieldValue['restriction'] as Restriction;
let min = '', max = '';
@@ -332,20 +383,18 @@
return { min, max };
};
const setSimpleValue = (value) => {
const setSimpleValue = (value: string) => {
// For special properties (eg. @dataType), we set the value directly
if (isSpecialProp) {
facet[fieldName] = value;
(facet as Record<string, unknown>)[fieldName] = value;
return;
}
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = { simpleValue: value };
(facet as Record<string, unknown>)[fieldName] = { simpleValue: value };
};
const setEnumerationValues = (values) => {
if (!facet[fieldName]) facet[fieldName] = {};
const setEnumerationValues = (values: string[]) => {
const enumItems = values.filter(v => v && typeof v === 'string' && v.trim() !== '').map(v => ({ '@value': v }));
facet[fieldName] = {
(facet as Record<string, unknown>)[fieldName] = {
'restriction': {
'@base': 'xs:string',
'enumeration': enumItems
@@ -353,9 +402,8 @@
};
};
const setPatternValue = (value) => {
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = {
const setPatternValue = (value: string) => {
(facet as Record<string, unknown>)[fieldName] = {
'restriction': {
'@base': 'xs:string',
'pattern': [{ '@value': value }]
@@ -363,25 +411,23 @@
};
};
const setRangeValues = (min, max, minType, maxType) => {
if (!facet[fieldName]) facet[fieldName] = {};
const restriction = { '@base': 'xs:string' };
const setRangeValues = (min: string, max: string, minType: string, maxType: string) => {
const restriction: Restriction = { '@base': 'xs:string' };
if (min !== '') {
const minKey = minType === 'Inclusive' ? 'minInclusive' : 'minExclusive';
restriction[minKey] = [{ '@value': min }];
(restriction as Record<string, RestrictionValue[]>)[minKey] = [{ '@value': min }];
}
if (max !== '') {
const maxKey = maxType === 'Inclusive' ? 'maxInclusive' : 'maxExclusive';
restriction[maxKey] = [{ '@value': max }];
(restriction as Record<string, RestrictionValue[]>)[maxKey] = [{ '@value': max }];
}
facet[fieldName] = { 'restriction': restriction };
(facet as Record<string, unknown>)[fieldName] = { 'restriction': restriction };
};
const setLengthValue = (value) => {
if (!facet[fieldName]) facet[fieldName] = {};
facet[fieldName] = {
const setLengthValue = (value: string) => {
(facet as Record<string, unknown>)[fieldName] = {
'restriction': {
'@base': 'xs:string',
'length': [{ '@value': value }]
@@ -389,20 +435,19 @@
};
};
const setLengthRangeValues = (min, max) => {
if (!facet[fieldName]) facet[fieldName] = {};
const restriction = { '@base': 'xs:string' };
const setLengthRangeValues = (min: string, max: string) => {
const restriction: Restriction = { '@base': 'xs:string' };
if (min !== '') restriction['minLength'] = [{ '@value': min }];
if (max !== '') restriction['maxLength'] = [{ '@value': max }];
facet[fieldName] = { 'restriction': restriction };
(facet as Record<string, unknown>)[fieldName] = { 'restriction': restriction };
};
let restrictionType = $derived(getRestrictionType());
let enumerationValues = $derived(getEnumerationValues());
const handleTypeChange = (newType) => {
const handleTypeChange = (newType: string) => {
restrictionType = newType;
switch (newType) {
@@ -435,23 +480,27 @@
enumerationValues = [...enumerationValues, ''];
};
const removeEnumerationValue = (index) => {
const removeEnumerationValue = (index: number) => {
enumerationValues = enumerationValues.filter((_, i) => i !== index);
setEnumerationValues(enumerationValues);
};
const updateEnumerationValue = (index, value) => {
const updateEnumerationValue = (index: number, value: string) => {
enumerationValues[index] = value;
setEnumerationValues(enumerationValues);
};
</script>
<div class="form-group">
<label>{label}</label>
<div class="restriction-controls">
<span class="form-label" id={`${baseId}-label`}>{label}</span>
<div class="restriction-controls" role="group" aria-labelledby={`${baseId}-label`}>
{#if !isSpecialProp}
<div class="restriction-type-selector">
<select class="form-input" bind:value={restrictionType} onchange={(e) => handleTypeChange(e.target.value)}>
<select
class="form-input"
bind:value={restrictionType}
onchange={(e) => handleTypeChange((e.target as HTMLSelectElement).value)}
>
<option value="Simple">Simple</option>
<option value="Enumeration">Enumeration</option>
<option value="Pattern">Pattern</option>
@@ -477,7 +526,7 @@
placeholder={placeholder}
/>
{:else}
<input class="form-input" type="text" bind:value={() => getSimpleValue(), (v) => setSimpleValue(v)} {placeholder}>
<input class="form-input" type="text" bind:value={() => getSimpleValue(), (v) => setSimpleValue(v)} {placeholder} aria-label={label}>
{/if}
{:else if restrictionType === 'Enumeration'}
@@ -497,7 +546,14 @@
placeholder={placeholder}
/>
{:else}
<input class="form-input" type="text" value={value} oninput={(e) => updateEnumerationValue(index, e.target.value)} {placeholder}>
<input
class="form-input"
type="text"
value={value}
oninput={(e) => updateEnumerationValue(index, (e.target as HTMLInputElement).value)}
{placeholder}
aria-label={`${label} option ${index + 1}`}
>
{/if}
<button class="btn-delete" onclick={() => removeEnumerationValue(index)} type="button" aria-label="Remove enumeration value">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -515,22 +571,42 @@
</div>
{:else if restrictionType === 'Pattern'}
<input class="form-input" type="text" bind:value={() => getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})">
<input class="form-input" type="text" bind:value={() => getPatternValue(), (v) => setPatternValue(v)} placeholder="Enter regex pattern (e.g., DT[0-9]{2})" aria-label={`${label} pattern`}>
{:else if restrictionType === 'Range'}
<div class="range-controls">
<div class="range-group">
<label>Min</label>
<input class="form-input" type="text" bind:value={() => getRangeValues().min, (v) => { const range = getRangeValues(); setRangeValues(v, range.max, range.minType, range.maxType); }} placeholder="0">
<select class="form-input" bind:value={() => getRangeValues().minType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, v, range.maxType); }}>
<label for={`${baseId}-range-min`}>Min</label>
<input
class="form-input"
type="text"
id={`${baseId}-range-min`}
bind:value={() => getRangeValues().min, (v) => { const range = getRangeValues(); setRangeValues(v, range.max, range.minType, range.maxType); }}
placeholder="0"
>
<select
class="form-input"
aria-label="Min bound type"
bind:value={() => getRangeValues().minType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, v, range.maxType); }}
>
<option value="Inclusive">Inclusive</option>
<option value="Exclusive">Exclusive</option>
</select>
</div>
<div class="range-group">
<label>Max</label>
<input class="form-input" type="text" bind:value={() => getRangeValues().max, (v) => { const range = getRangeValues(); setRangeValues(range.min, v, range.minType, range.maxType); }} placeholder="0">
<select class="form-input" bind:value={() => getRangeValues().maxType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, range.minType, v); }}>
<label for={`${baseId}-range-max`}>Max</label>
<input
class="form-input"
type="text"
id={`${baseId}-range-max`}
bind:value={() => getRangeValues().max, (v) => { const range = getRangeValues(); setRangeValues(range.min, v, range.minType, range.maxType); }}
placeholder="0"
>
<select
class="form-input"
aria-label="Max bound type"
bind:value={() => getRangeValues().maxType, (v) => { const range = getRangeValues(); setRangeValues(range.min, range.max, range.minType, v); }}
>
<option value="Inclusive">Inclusive</option>
<option value="Exclusive">Exclusive</option>
</select>
@@ -538,17 +614,17 @@
</div>
{:else if restrictionType === 'Length'}
<input class="form-input" type="number" bind:value={() => getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length">
<input class="form-input" type="number" bind:value={() => getLengthValue(), (v) => setLengthValue(v)} placeholder="Enter exact length" aria-label={`${label} length`}>
{:else if restrictionType === 'Length Range'}
<div class="length-range-controls">
<div class="length-group">
<label>Min Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().min, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(v, range.max); }} placeholder="0">
<label for={`${baseId}-length-min`}>Min Length</label>
<input class="form-input" id={`${baseId}-length-min`} type="number" bind:value={() => getLengthRangeValues().min, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(v, range.max); }} placeholder="0">
</div>
<div class="length-group">
<label>Max Length</label>
<input class="form-input" type="number" bind:value={() => getLengthRangeValues().max, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(range.min, v); }} placeholder="0">
<label for={`${baseId}-length-max`}>Max Length</label>
<input class="form-input" id={`${baseId}-length-max`} type="number" bind:value={() => getLengthRangeValues().max, (v) => { const range = getLengthRangeValues(); setLengthRangeValues(range.min, v); }} placeholder="0">
</div>
</div>
{/if}
@@ -656,4 +732,4 @@
color: #666;
margin: 0;
}
</style>
</style>
@@ -1,33 +1,44 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import type { DocumentState, IdsCardinality, IdsDocument, Specification } from "$src/types/ids";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
const getProp = (prop) => {
return activeSpecification?.[prop] ?? "";
const getProp = (prop: string) => {
const value = (activeSpecification as Record<string, unknown>)?.[prop];
return typeof value === "string" ? value : "";
};
const setProp = (prop, value) => {
activeSpecification[prop] = value;
const setProp = (prop: string, value: string) => {
if (!activeSpecification) return;
(activeSpecification as Record<string, unknown>)[prop] = value;
};
const addIfcVersion = (e, version) => {
const addIfcVersion = (e: Event, version: string) => {
if (activeSpecification) {
if (!("@ifcVersion" in activeSpecification)) activeSpecification["@ifcVersion"] = [];
if (e.target.checked) {
if (!activeSpecification["@ifcVersion"].includes(version)) {
activeSpecification["@ifcVersion"] = [...activeSpecification["@ifcVersion"], version];
const target = e.target as HTMLInputElement | null;
if (target?.checked) {
if (!activeSpecification["@ifcVersion"]?.includes(version)) {
activeSpecification["@ifcVersion"] = [...(activeSpecification["@ifcVersion"] ?? []), version];
}
} else {
} else if (activeSpecification["@ifcVersion"]) {
activeSpecification["@ifcVersion"] = activeSpecification["@ifcVersion"].filter(v => v !== version);
}
}
};
const setUsage = (usage) => {
const setUsage = (usage: IdsCardinality) => {
if (!activeSpecification) return;
if (!activeSpecification.applicability) activeSpecification.applicability = {};
@@ -60,14 +71,19 @@
</div>
<div class="form-group">
<label for="spec-cardinality">Usage</label>
<select class="form-input" id="spec-cardinality" value={IDS.getSpecUsage(activeSpecification)} onchange={(e) => setUsage(e.target.value)}>
<select
class="form-input"
id="spec-cardinality"
value={IDS.getSpecUsage(activeSpecification)}
onchange={(e) => setUsage((e.target as HTMLSelectElement).value as IdsCardinality)}
>
<option value="required">Required</option>
<option value="optional">Optional</option>
<option value="prohibited">Prohibited</option>
</select>
</div>
<div class="form-group full-width">
<label>IFC Version</label>
<span class="form-label">IFC Version</span>
<div class="radio-group">
<label class="radio-label">
<input type="checkbox" value="IFC2X3" checked={activeSpecification?.["@ifcVersion"]?.includes('IFC2X3')} onchange={(e) => addIfcVersion(e, 'IFC2X3')}>
@@ -92,4 +108,4 @@
<textarea class="form-input" id="spec-instructions" bind:value={() => getProp("@instructions"), (v) => setProp("@instructions", v)} placeholder="Enter instructions" rows="3"></textarea>
</div>
</div>
</div>
</div>
@@ -1,5 +1,5 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import AppHeader from "$src/components/AppHeader.svelte";
import AppRibbon from "$src/components/AppRibbon.svelte";
import AppToolbar from "$src/components/AppToolbar.svelte";
@@ -11,14 +11,21 @@
import IdsViewer from "./IdsViewer.svelte";
import SplashScreen from "$src/components/SplashScreen.svelte";
import { Toaster } from "$lib/components/ui/sonner";
import { error, success } from "$src/modules/utils/toast.svelte.js";
import {onMount} from "svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import type { DocumentState, IdsDocument, Specification } from "$src/types/ids";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let activeDocument = $derived(IDS.Module.activeDocument ? IDS.Module.documents[IDS.Module.activeDocument] : null);
let documentState = $derived(IDS.Module.activeDocument ? IDS.Module.states[IDS.Module.activeDocument] : null);
let activeSpecification = $derived(activeDocument && documentState?.activeSpecification !== null && activeDocument.specifications?.specification ?
activeDocument.specifications.specification[documentState.activeSpecification] : null);
let activeDocument = $derived(
IDS.Module.activeDocument ? (IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument) : null
);
let documentState = $derived(
IDS.Module.activeDocument ? (IDS.Module.states[IDS.Module.activeDocument] as DocumentState) : null
);
let activeSpecification = $derived(
activeDocument && documentState && documentState.activeSpecification !== null && activeDocument.specifications?.specification
? (activeDocument.specifications.specification[documentState.activeSpecification] as Specification)
: null
);
async function addNewSpecification() {
if (!IDS.Module.activeDocument) return;
@@ -28,11 +35,11 @@
IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'info' });
}
async function importSpecification(sourceDocId, specIndex) {
async function importSpecification(sourceDocId: string, specIndex: number) {
if (!IDS.Module.activeDocument || !IDS.Module.documents[sourceDocId]) return;
try {
const sourceDoc = IDS.Module.documents[sourceDocId];
const sourceDoc = IDS.Module.documents[sourceDocId] as IdsDocument;
const sourceSpec = sourceDoc.specifications?.specification?.[specIndex];
if (!sourceSpec) {
@@ -41,13 +48,13 @@
}
// Create a deep copy of the specification
const specCopy = JSON.parse(JSON.stringify(sourceSpec));
const specCopy = JSON.parse(JSON.stringify(sourceSpec)) as Specification;
// Add the copied specification to the current document
IDS.Module.documents[IDS.Module.activeDocument].specifications.specification.push(specCopy);
// Switch to the newly created specification and info tab
const currentDoc = IDS.Module.documents[IDS.Module.activeDocument];
const currentDoc = IDS.Module.documents[IDS.Module.activeDocument] as IdsDocument;
const newSpecIndex = (currentDoc.specifications?.specification?.length || 1) - 1;
IDS.setDocumentState(IDS.Module.activeDocument, {
activeSpecification: newSpecIndex,
@@ -57,17 +64,18 @@
success(`Specification "${sourceSpec['@name'] || 'Unnamed'}" imported successfully`);
} catch (err) {
console.error('Error importing specification:', err);
error(`Failed to import specification: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to import specification: ${message}`);
}
}
function selectSpecification(index) {
function selectSpecification(index: number) {
if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: index });
}
}
async function deleteSpecification(specIndex) {
async function deleteSpecification(specIndex: number) {
if (!IDS.Module.activeDocument) return;
await IDS.deleteSpecification(IDS.Module.activeDocument, specIndex);
}
@@ -83,9 +91,24 @@
success('IDS document exported successfully');
} catch (err) {
console.error('Error exporting IDS:', err);
error('Error exporting IDS: ' + err.message);
const message = err instanceof Error ? err.message : String(err);
error('Error exporting IDS: ' + message);
}
}
const updateActiveDocumentState = (updates: Partial<DocumentState>) => {
if (IDS.Module.activeDocument) {
IDS.setDocumentState(IDS.Module.activeDocument, updates);
}
};
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.currentTarget !== event.target) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script>
{#if IDS.Module.status != "ready"}
@@ -162,16 +185,30 @@
</DropdownMenu.Root>
</div>
<div class="specifications-list scrollbar">
<div class="spec-item" class:active={documentState?.activeSpecification === null} onclick={() => { if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); }}>
<div
class="spec-item"
class:active={documentState?.activeSpecification === null}
role="button"
tabindex="0"
onclick={() => { if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); }}
onkeydown={(event) => handleActivation(event, () => { if (IDS.Module.activeDocument) IDS.setDocumentState(IDS.Module.activeDocument, { activeSpecification: null }); })}
>
<span class="spec-icon"></span>
<span class="spec-name">IDS Information</span>
</div>
{#if activeDocument?.specifications?.specification}
{#each activeDocument.specifications.specification as spec, index}
<div class="spec-item" class:active={documentState?.activeSpecification === index} onclick={() => selectSpecification(index)}>
<div
class="spec-item"
class:active={documentState?.activeSpecification === index}
role="button"
tabindex="0"
onclick={() => selectSpecification(index)}
onkeydown={(event) => handleActivation(event, () => selectSpecification(index))}
>
<span class="spec-icon">📄</span>
<span class="spec-name">{spec["@name"] || "Specification " + (index + 1)}</span>
<button class="btn-delete" onclick={() => deleteSpecification(index)}>
<button class="btn-delete" onclick={() => deleteSpecification(index)} aria-label="Delete specification">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
@@ -192,10 +229,10 @@
{:else}
<!-- Editor/Viewer Toggle -->
<div class="view-mode-toggle">
<button class="toggle-btn" class:active={documentState?.viewMode === 'editor'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { viewMode: 'editor', auditReport: null })}>
<button class="toggle-btn" class:active={documentState?.viewMode === 'editor'} onclick={() => updateActiveDocumentState({ viewMode: 'editor', auditReport: null })}>
Editor
</button>
<button class="toggle-btn" class:active={documentState?.viewMode === 'viewer'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { viewMode: 'viewer', auditReport: null })}>
<button class="toggle-btn" class:active={documentState?.viewMode === 'viewer'} onclick={() => updateActiveDocumentState({ viewMode: 'viewer', auditReport: null })}>
Viewer
</button>
</div>
@@ -209,18 +246,18 @@
<div class="spec-header">
<h2>{activeSpecification ? activeSpecification["@name"] || "Specification" : "Specification"}</h2>
<div class="spec-tabs">
<button class="btn tab-btn" class:active={documentState?.activeTab === 'info'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'info' })}>Info</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'applicability'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'applicability' })}>Applicability</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'requirements'} onclick={() => IDS.setDocumentState(IDS.Module.activeDocument, { activeTab: 'requirements' })}>Requirements</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'info'} onclick={() => updateActiveDocumentState({ activeTab: 'info' })}>Info</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'applicability'} onclick={() => updateActiveDocumentState({ activeTab: 'applicability' })}>Applicability</button>
<button class="btn tab-btn" class:active={documentState?.activeTab === 'requirements'} onclick={() => updateActiveDocumentState({ activeTab: 'requirements' })}>Requirements</button>
</div>
</div>
{#if documentState?.activeTab === 'info'}
<SpecificationEditor />
{:else if documentState?.activeTab === 'applicability'}
<ApplicabilityPanel activeTab />
<ApplicabilityPanel />
{:else if documentState?.activeTab === 'requirements'}
<RequirementsPanel activeTab />
<RequirementsPanel />
{/if}
</div>
{/if}
+70
View File
@@ -0,0 +1,70 @@
export type IdsCardinality = "required" | "optional" | "prohibited";
export type RestrictionValue = {
"@value": string;
};
export type Restriction = {
"@base"?: string;
enumeration?: RestrictionValue[];
pattern?: RestrictionValue[];
length?: RestrictionValue[];
minLength?: RestrictionValue[];
maxLength?: RestrictionValue[];
minInclusive?: RestrictionValue[];
maxInclusive?: RestrictionValue[];
minExclusive?: RestrictionValue[];
maxExclusive?: RestrictionValue[];
};
export type SimpleValue = {
simpleValue: string;
};
export type FacetValue = {
simpleValue?: string;
restriction?: Restriction;
};
export type Facet = Record<string, FacetValue | string | number | boolean | null | undefined>;
export type FacetClause = Record<string, Facet[] | number | "unbounded" | undefined>;
export type Specification = {
"@name"?: string;
"@identifier"?: string;
"@description"?: string;
"@instructions"?: string;
"@ifcVersion"?: string[];
applicability?: FacetClause;
requirements?: FacetClause;
};
export type IdsInfo = {
title?: string;
copyright?: string;
version?: string;
description?: string;
author?: string;
date?: string;
purpose?: string;
milestone?: string;
};
export type IdsDocument = {
"@xmlns"?: string;
"@xmlns:xs"?: string;
"@xmlns:xsi"?: string;
"@xsi:schemaLocation"?: string;
info: IdsInfo;
specifications: {
specification: Specification[];
};
};
export type DocumentState = {
activeTab: "info" | "applicability" | "requirements";
viewMode: "editor" | "viewer";
activeSpecification: number | null;
auditReport?: string | null;
};
+93
View File
@@ -0,0 +1,93 @@
export type ResultsPercent = number | "N/A";
export type AuditReportEntity = {
reason?: string;
element?: unknown;
element_type?: unknown;
class?: string;
predefined_type?: string;
name?: string | null;
description?: string | null;
id?: number;
global_id?: string | null;
tag?: string | null;
type_name?: string;
type_tag?: string | null;
type_global_id?: string | null;
extra_of_type?: number;
};
export type AuditRequirement = {
facet_type: string;
metadata: Record<string, unknown>;
label: string;
value: string;
description: string;
status: boolean;
passed_entities: AuditReportEntity[];
failed_entities: AuditReportEntity[];
total_applicable: number;
total_pass: number;
total_fail: number;
percent_pass: ResultsPercent;
instructions?: string;
total_failed_entities?: number;
total_omitted_failures?: number;
has_omitted_failures?: boolean;
total_passed_entities?: number;
total_omitted_passes?: number;
has_omitted_passes?: boolean;
};
export type AuditSpecification = {
name: string;
description: string;
instructions: string;
status: boolean;
is_ifc_version: boolean;
total_applicable: number;
total_applicable_pass: number;
total_applicable_fail: number;
percent_applicable_pass: ResultsPercent;
total_checks: number;
total_checks_pass: number;
total_checks_fail: number;
percent_checks_pass: ResultsPercent;
cardinality: string;
applicability: string[];
requirements: AuditRequirement[];
total_requirements?: number;
total_requirements_pass?: number;
};
export type AuditReportData = {
title: string;
date: string;
filepath: string | null;
filename: string | null;
hide_skipped: boolean;
specifications: AuditSpecification[];
status: boolean;
total_specifications: number;
total_specifications_pass: number;
total_specifications_fail: number;
percent_specifications_pass: ResultsPercent;
total_requirements: number;
total_requirements_pass: number;
total_requirements_fail: number;
percent_requirements_pass: ResultsPercent;
total_checks: number;
total_checks_pass: number;
total_checks_fail: number;
percent_checks_pass: ResultsPercent;
};
export type AuditReport = {
id: string;
modelId: string;
modelName: string;
document: string;
date: string;
data: AuditReportData;
htmlReport?: string | null;
};
+24
View File
@@ -0,0 +1,24 @@
export type WorkerMessageType =
| "init"
| "api_call"
| "ready"
| "api_response"
| "error"
| "disposed";
export type WorkerRequest = {
type: WorkerMessageType;
payload?: Record<string, unknown>;
id: string;
};
export type WorkerResponse = {
type: WorkerMessageType;
payload?: Record<string, unknown>;
id: string;
};
export type ApiCallPayload = {
method: string;
args?: unknown[];
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"baseUrl": ".",
"paths": {
"$lib": ["./src/lib"],
"$lib/*": ["./src/lib/*"],
"$src": ["./src"],
"$src/*": ["./src/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"]
}
+2 -2
View File
@@ -1,7 +1,7 @@
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from "path";
import path from "node:path";
export default defineConfig({
plugins: [tailwindcss(), svelte()],
@@ -11,4 +11,4 @@ export default defineConfig({
$src: path.resolve("./src"),
},
},
});
});