@miadi/stateloom-web 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/server/.next/BUILD_ID +1 -1
  3. package/server/.next/app-path-routes-manifest.json +1 -0
  4. package/server/.next/build-manifest.json +2 -2
  5. package/server/.next/prerender-manifest.json +3 -3
  6. package/server/.next/required-server-files.json +1 -1
  7. package/server/.next/routes-manifest.json +6 -0
  8. package/server/.next/server/app/_global-error.html +2 -2
  9. package/server/.next/server/app/_global-error.rsc +1 -1
  10. package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/server/.next/server/app/_not-found.html +1 -1
  16. package/server/.next/server/app/_not-found.rsc +1 -1
  17. package/server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  18. package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  20. package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  22. package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/server/.next/server/app/api/docs/route.js.nft.json +1 -1
  24. package/server/.next/server/app/api/file/route.js.nft.json +1 -1
  25. package/server/.next/server/app/api/marks/route.js.nft.json +1 -1
  26. package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
  27. package/server/.next/server/app/api/system-of/route/app-paths-manifest.json +3 -0
  28. package/server/.next/server/app/api/system-of/route/build-manifest.json +11 -0
  29. package/server/.next/server/app/api/system-of/route/server-reference-manifest.json +4 -0
  30. package/server/.next/server/app/api/system-of/route.js +8 -0
  31. package/server/.next/server/app/api/system-of/route.js.map +5 -0
  32. package/server/.next/server/app/api/system-of/route.js.nft.json +1 -0
  33. package/server/.next/server/app/api/system-of/route_client-reference-manifest.js +2 -0
  34. package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
  35. package/server/.next/server/app/index.html +1 -1
  36. package/server/.next/server/app/index.rsc +2 -2
  37. package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  38. package/server/.next/server/app/index.segments/_full.segment.rsc +2 -2
  39. package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/server/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/server/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/server/.next/server/app-paths-manifest.json +1 -0
  44. package/server/.next/server/chunks/[root-of-the-server]__13c3d2a8._.js +1 -1
  45. package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
  46. package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
  47. package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +1 -1
  48. package/server/.next/server/chunks/[root-of-the-server]__aedef6d1._.js +1 -1
  49. package/server/.next/server/chunks/[root-of-the-server]__d21ea469._.js +3 -0
  50. package/server/.next/server/chunks/bridge-protocol_dist_index_179d02a5.js +1 -1
  51. package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +3 -3
  52. package/server/.next/server/chunks/web__next-internal_server_app_api_system-of_route_actions_1988181c.js +3 -0
  53. package/server/.next/server/pages/404.html +1 -1
  54. package/server/.next/server/pages/500.html +2 -2
  55. package/server/.next/server/server-reference-manifest.js +1 -1
  56. package/server/.next/server/server-reference-manifest.json +1 -1
  57. package/server/.next/static/chunks/{b0af2862e9bb3e18.js → 0fd0d2a3caf9e5c4.js} +3 -3
  58. package/server/package-lock.json +2 -2
  59. package/server/package.json +1 -1
  60. package/server/server.js +1 -1
  61. package/server/src/app/api/system-of/route.ts +22 -0
  62. package/server/src/app/page.tsx +4 -2
  63. package/server/src/lib/systemOf.ts +80 -0
  64. package/server/src/lib/systemParam.ts +46 -1
  65. package/server/stateloom-web.json +2 -2
  66. /package/server/.next/static/{ZoijkpoWD861NG-cIGJrO → 1YtenfowImzXV-wx5GWY1}/_buildManifest.js +0 -0
  67. /package/server/.next/static/{ZoijkpoWD861NG-cIGJrO → 1YtenfowImzXV-wx5GWY1}/_clientMiddlewareManifest.json +0 -0
  68. /package/server/.next/static/{ZoijkpoWD861NG-cIGJrO → 1YtenfowImzXV-wx5GWY1}/_ssgManifest.js +0 -0
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-web-source",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@miadi/stateloom-web-source",
9
- "version": "0.3.4",
9
+ "version": "0.3.5",
10
10
  "dependencies": {
11
11
  "@miadi/stateloom-canvas": "file:../bridge-canvas",
12
12
  "@miadi/stateloom-client": "file:../bridge-client",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-web-source",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "private": true,
5
5
  "//": [
6
6
  "This is the designer's SOURCE and is never published — hence private: true.",
package/server/server.js CHANGED
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{},"webpack":null,"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/workspace/repos/jgwill/smcraft","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"dynamicOnHover":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":11,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":false,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":false,"lockDistDir":true,"isolatedDevBuild":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":false,"optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["**/node_modules/typescript/**","**/node_modules/@img/**","**/node_modules/sharp/**"]},"turbopack":{"root":"/workspace/repos/jgwill/smcraft"},"transpilePackages":["@miadi/stateloom-protocol","@miadi/stateloom-client","@miadi/stateloom-react"],"distDirRoot":".next"}
12
+ const nextConfig = {"env":{},"webpack":null,"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/workspace/repos/jgwill/smcraft","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"dynamicOnHover":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":5,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":false,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":false,"lockDistDir":true,"isolatedDevBuild":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":false,"optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["**/node_modules/typescript/**","**/node_modules/@img/**","**/node_modules/sharp/**"]},"turbopack":{"root":"/workspace/repos/jgwill/smcraft"},"transpilePackages":["@miadi/stateloom-protocol","@miadi/stateloom-client","@miadi/stateloom-react"],"distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * GET /api/system-of?doc=<absolute path>[&system=<.sysdf.json>] — the system
3
+ * that lists the document.
4
+ *
5
+ * `{ doc, system }`: `doc` as the server resolved it, `system` the given system
6
+ * when it lists the document, else the nearest `.sysdf.json` that does, else
7
+ * null (lib/systemOf.ts). A doc the file API would refuse is refused here the
8
+ * same way, a 403 with its reason.
9
+ */
10
+ import { NextRequest, NextResponse } from "next/server";
11
+ import { systemOfDoc } from "@/lib/systemOf";
12
+
13
+ export const dynamic = "force-dynamic";
14
+ export const revalidate = 0;
15
+
16
+ export async function GET(req: NextRequest): Promise<Response> {
17
+ const doc = req.nextUrl.searchParams.get("doc");
18
+ if (!doc) return NextResponse.json({ error: "doc is required" }, { status: 400 });
19
+ const found = systemOfDoc(doc, req.nextUrl.searchParams.get("system"));
20
+ if (!found.ok) return NextResponse.json({ error: found.error }, { status: 403 });
21
+ return NextResponse.json({ doc: found.doc, system: found.system }, { headers: { "cache-control": "no-store" } });
22
+ }
@@ -25,7 +25,7 @@ import { useRequestedDoc } from "@/lib/docParam";
25
25
  import { useMachineFocus } from "@/lib/machineFocus";
26
26
  import { loadRuntimeConfig } from "@/lib/runtimeConfig";
27
27
  import { useSheetDrag } from "@/lib/useSheetDrag";
28
- import { useUrlParam } from "@/lib/systemParam";
28
+ import { useSystemOfDoc, useUrlParam } from "@/lib/systemParam";
29
29
 
30
30
  type Tab = "properties" | "events" | "settings" | "validation";
31
31
 
@@ -65,12 +65,14 @@ function contextMenuPlacement(x: number, y: number): CSSProperties {
65
65
  * process names its default document.
66
66
  *
67
67
  * With `?system=<.sysdf.json>` the document is read as a member of that system,
68
- * and the system strip sits above whichever workspace is open.
68
+ * and the system strip sits above whichever workspace is open. Without it, the
69
+ * system that lists the document is looked up and written there.
69
70
  */
70
71
  export default function Home() {
71
72
  // `null` until its first effect has read the URL, and whenever there is no `?doc=`.
72
73
  const requested = useRequestedDoc();
73
74
  const systemParam = useUrlParam("system");
75
+ useSystemOfDoc(requested, systemParam);
74
76
  const [defaultKind, setDefaultKind] = useState<DocKind | null>(null);
75
77
 
76
78
  useEffect(() => {
@@ -0,0 +1,80 @@
1
+ import { readFileSync, readdirSync, realpathSync } from "fs";
2
+ import { dirname, join, sep } from "path";
3
+ import { isSysdfPath, isSystemDefinition, resolveMemberPath } from "@miadi/stateloom-protocol";
4
+ import { allowedDocRoots, chronicleRoot, resolveDocPath } from "./projectFile";
5
+
6
+ /** How many folders above the document are searched. */
7
+ const SEARCH_DEPTH = 8;
8
+
9
+ const realOr = (path: string): string => {
10
+ try {
11
+ return realpathSync(path);
12
+ } catch {
13
+ return path;
14
+ }
15
+ };
16
+
17
+ /** Could the file API serve something in `dir`, or in a folder below it? */
18
+ function underAnAdmittedRoot(dir: string): boolean {
19
+ const roots = allowedDocRoots();
20
+ const chronicle = chronicleRoot();
21
+ if (chronicle) roots.push(chronicle);
22
+ return roots.some((root) => dir === root || dir.startsWith(root + sep));
23
+ }
24
+
25
+ export type SystemOfResult = { ok: true; doc: string; system: string | null } | { ok: false; error: string };
26
+
27
+ /** The admitted, readable system at `path` lists `target`: its resolved path, else null. */
28
+ function listing(path: string, target: string): string | null {
29
+ const candidate = resolveDocPath(path);
30
+ if (!candidate.ok || !isSysdfPath(candidate.path)) return null;
31
+ let def: unknown;
32
+ try {
33
+ def = JSON.parse(readFileSync(candidate.path, "utf8"));
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (!isSystemDefinition(def)) return null;
38
+ const listed = def.members.some((m) => !!m?.path && realOr(resolveMemberPath(candidate.path, m.path)) === target);
39
+ return listed ? candidate.path : null;
40
+ }
41
+
42
+ /**
43
+ * The system that lists `doc` (Spec 82), so a drawing opened alone (`?doc=`
44
+ * with no `system=`, the link an agent prints) is still read as a member of its
45
+ * system and the strip can lead back to the map and to the other drawings.
46
+ *
47
+ * `preferred` (the `system=` already in the URL) answers when it lists the
48
+ * document: a drawing two systems share stays in the one the person came from.
49
+ * Otherwise the rule is the MCP's (`systemFor` in mcp/src/system.ts): the first
50
+ * `.sysdf.json`, in name order, in the document's own folder and then in each
51
+ * parent folder, that names the document as a member. Every candidate goes
52
+ * through `resolveDocPath`, so this reads only systems the file API would serve
53
+ * and never names a path it refuses. The walk stops at the admitted roots.
54
+ */
55
+ export function systemOfDoc(doc: string, preferred?: string | null): SystemOfResult {
56
+ const resolution = resolveDocPath(doc);
57
+ if (!resolution.ok) return { ok: false, error: resolution.error };
58
+ const target = resolution.path;
59
+ if (isSysdfPath(target)) return { ok: true, doc: target, system: null };
60
+ const kept = preferred ? listing(preferred, target) : null;
61
+ if (kept) return { ok: true, doc: target, system: kept };
62
+
63
+ let dir = dirname(target);
64
+ for (let depth = 0; depth <= SEARCH_DEPTH && underAnAdmittedRoot(dir); depth += 1) {
65
+ let names: string[] = [];
66
+ try {
67
+ names = readdirSync(dir).filter((f) => !f.startsWith(".") && isSysdfPath(f)).sort();
68
+ } catch {
69
+ // An unreadable folder lists nothing.
70
+ }
71
+ for (const name of names) {
72
+ const system = listing(join(dir, name), target);
73
+ if (system) return { ok: true, doc: target, system };
74
+ }
75
+ const parent = dirname(dir);
76
+ if (parent === dir) break;
77
+ dir = parent;
78
+ }
79
+ return { ok: true, doc: target, system: null };
80
+ }
@@ -15,13 +15,17 @@
15
15
  * the strip can list its links.
16
16
  * - `stateloom:system-changed`: a member or the system file was written; every
17
17
  * holder of a loaded system reloads it.
18
+ * - `stateloom:system-found`: `system=` was written for the document already
19
+ * open (`useSystemOfDoc`); nothing else moved.
18
20
  */
19
21
  import { useEffect, useState } from "react";
22
+ import { isSysdfPath } from "@miadi/stateloom-protocol";
20
23
  import { DOC_CHANGE_EVENT } from "./docParam";
21
24
 
22
25
  export const FOCUS_EVENT = "stateloom:focus";
23
26
  export const FOCUS_REQUEST_EVENT = "stateloom:focus-request";
24
27
  export const SYSTEM_CHANGED_EVENT = "stateloom:system-changed";
28
+ export const SYSTEM_FOUND_EVENT = "stateloom:system-found";
25
29
  /** A one-line note for the system strip: `{ detail: { text, origin? } }`. */
26
30
  export const STRIP_NOTE_EVENT = "stateloom:strip-note";
27
31
 
@@ -36,7 +40,7 @@ export function useUrlParam(name: string): string | null {
36
40
  useEffect(() => {
37
41
  const update = () => setValue(urlParam(name));
38
42
  update();
39
- const events = ["popstate", DOC_CHANGE_EVENT, FOCUS_EVENT, FOCUS_REQUEST_EVENT];
43
+ const events = ["popstate", DOC_CHANGE_EVENT, FOCUS_EVENT, FOCUS_REQUEST_EVENT, SYSTEM_FOUND_EVENT];
40
44
  for (const e of events) window.addEventListener(e, update);
41
45
  return () => {
42
46
  for (const e of events) window.removeEventListener(e, update);
@@ -45,6 +49,47 @@ export function useUrlParam(name: string): string | null {
45
49
  return value;
46
50
  }
47
51
 
52
+ /**
53
+ * Read the open drawing as a member of the system that lists it. A drawing
54
+ * opened alone (`?doc=` and no `system=`, the link an agent prints) had no
55
+ * strip, so nothing led back to the map or on to the other drawings. And a
56
+ * drawing reached by the document switcher kept the strip of the system it was
57
+ * left from, even when another system lists it.
58
+ *
59
+ * The server answers (`/api/system-of`, lib/systemOf.ts): the system already in
60
+ * the URL when it lists the drawing, else the nearest one that does. When it
61
+ * names another system, `system=` is rewritten in place, without a history
62
+ * entry: the person did not move, the view learned where it is. When no system
63
+ * lists the drawing, the URL is left as it is, and a strip already on screen
64
+ * stays while the person browses.
65
+ */
66
+ export function useSystemOfDoc(doc: string | null, system: string | null): void {
67
+ useEffect(() => {
68
+ if (!doc || isSysdfPath(doc)) return;
69
+ let live = true;
70
+ const query = new URLSearchParams({ doc });
71
+ if (system) query.set("system", system);
72
+ fetch(`/api/system-of?${query.toString()}`, { cache: "no-store" })
73
+ .then((r) => (r.ok ? r.json() : null))
74
+ .then((body: { system?: string | null } | null) => {
75
+ const found = body?.system;
76
+ if (!live || !found || found === system) return;
77
+ // The person moved on while the server looked: this answer is for a view that is gone.
78
+ if (urlParam("doc") !== doc || urlParam("system") !== system) return;
79
+ const url = new URL(window.location.href);
80
+ url.searchParams.set("system", found);
81
+ window.history.replaceState(window.history.state, "", url);
82
+ window.dispatchEvent(new Event(SYSTEM_FOUND_EVENT));
83
+ })
84
+ .catch(() => {
85
+ // An older server has no /api/system-of: the drawing opens alone, as before.
86
+ });
87
+ return () => {
88
+ live = false;
89
+ };
90
+ }, [doc, system]);
91
+ }
92
+
48
93
  /** The query string for a member opened inside a system, focused on one element. */
49
94
  export function systemQuery(target: { doc: string; system?: string | null; focus?: string | null }): string {
50
95
  const params = new URLSearchParams();
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.3.4",
3
- "designer": "0.3.4",
2
+ "package": "0.3.5",
3
+ "designer": "0.3.5",
4
4
  "next": "16.1.6"
5
5
  }