labull-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +82 -0
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.js +17 -0
  5. package/dist/chunk-_plugin-vue_export-helper-CHgC5LLL.js +9 -0
  6. package/dist/chunk-index-BWpDbB19.js +129 -0
  7. package/dist/chunk-index-_jasBCim.js +526 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +30 -0
  10. package/dist/magic-block.d.ts +1 -0
  11. package/dist/magic-block.js +1580 -0
  12. package/dist/style.css +1 -0
  13. package/dist/theme.d.ts +1 -0
  14. package/dist/theme.js +4 -0
  15. package/dist/types/auth-real.d.ts +60 -0
  16. package/dist/types/auth.d.ts +1 -0
  17. package/dist/types/index.d.ts +2 -0
  18. package/dist/types/magic-block-MagicBlockConsole.vue.d.ts +2 -0
  19. package/dist/types/magic-block-MagicBlockHost.vue.d.ts +21 -0
  20. package/dist/types/magic-block-bridge.d.ts +22 -0
  21. package/dist/types/magic-block-devkit.d.ts +15 -0
  22. package/dist/types/magic-block-instances.d.ts +34 -0
  23. package/dist/types/magic-block-localfs.d.ts +126 -0
  24. package/dist/types/magic-block-manifest.d.ts +64 -0
  25. package/dist/types/magic-block-preview.d.ts +11 -0
  26. package/dist/types/magic-block-real.d.ts +7 -0
  27. package/dist/types/magic-block-render.d.ts +12 -0
  28. package/dist/types/magic-block-theme.d.ts +33 -0
  29. package/dist/types/magic-block.d.ts +1 -0
  30. package/dist/types/theme.d.ts +3 -0
  31. package/dist/types/ui-AppSearch.vue.d.ts +2 -0
  32. package/dist/types/ui-ProductShell.vue.d.ts +31 -0
  33. package/dist/types/ui-ProductSide.vue.d.ts +14 -0
  34. package/dist/types/ui-ProductTabs.vue.d.ts +2 -0
  35. package/dist/types/ui-UserMenu.vue.d.ts +2 -0
  36. package/dist/types/ui-gate.d.ts +9 -0
  37. package/dist/types/ui-real.d.ts +16 -0
  38. package/dist/types/ui-tabs.d.ts +31 -0
  39. package/dist/types/ui-types.d.ts +20 -0
  40. package/dist/types/ui.d.ts +1 -0
  41. package/dist/ui.d.ts +1 -0
  42. package/dist/ui.js +15 -0
  43. package/package.json +67 -0
@@ -0,0 +1 @@
1
+ export * from './types/theme'
package/dist/theme.js ADDED
@@ -0,0 +1,4 @@
1
+ const e = "labull-framework/theme";
2
+ export {
3
+ e as THEME_ENTRY
4
+ };
@@ -0,0 +1,60 @@
1
+ import { type Ref } from 'vue';
2
+ export declare const API_BASE: string;
3
+ export declare const AUTH_DEADLINE_MS = 8000;
4
+ export declare const CLEAR_SESSION_DEADLINE_MS = 1500;
5
+ export interface AuthUser {
6
+ id: string;
7
+ /** Authing 的身份(就是 Django 那边的 `username`)。 */
8
+ username: string;
9
+ name: string;
10
+ /** 工号(Authing 的 `username` 同步过来的;没有就是 null)。 */
11
+ workId: string | null;
12
+ /** 🔴 "是不是管理员" = 这个布尔位(Django 的 `is_staff`)。前端只拿它做**显隐**,授权全在后端。 */
13
+ isStaff: boolean;
14
+ groups: string[];
15
+ permissions: string[];
16
+ }
17
+ export declare function authHeaders(method?: string, json?: boolean): Record<string, string>;
18
+ export declare function apiFetch<T>(path: string, init?: RequestInit): Promise<T>;
19
+ export declare function currentPath(): string;
20
+ /** 后端登录入口(它再 302 到 OIDC 授权页)。🔴 只放行站内 redirect:放行外站就是开放重定向。 */
21
+ export declare function loginUrl(redirect?: string): string;
22
+ export declare function loginWithRedirect(redirect?: string): void;
23
+ export declare function logout(): void;
24
+ /**
25
+ * 认人。**只有后端明确说"你没登录"(401 / 403)才去清会话**。
26
+ *
27
+ * 🔴 这里曾经有个很难查的 bug:原来"只要 `/auth/me` 不顺就 POST `/auth/logout`",
28
+ * 而网络抖动、后端还没起来、代理 502 都会被算进"不顺" ⇒ 顺手把**整个会话**清掉。
29
+ * 登录流程的 `state` 正存在那个会话里(`/auth/login` 存、`/auth/callback` 比),
30
+ * 于是症状极难对上:**"点登录 → 走完 Authing → 回到 /auth/callback 说登录会话已过期"**,
31
+ * 而中间那一下"清会话"跟登录两个字在日志里一点关系都看不出来。
32
+ * 🔴 另外"后端没答话"本来就**判不出来**是谁——那时什么都不该做(下一次问就是了)。
33
+ */
34
+ export declare function currentUser(): Promise<AuthUser | null>;
35
+ export declare function currentUserInTime(ms?: number): Promise<AuthUser | null | 'timeout'>;
36
+ export declare function ensureAuthenticated(redirect?: string, ms?: number): Promise<AuthUser | null>;
37
+ export interface AuthGate {
38
+ open: Ref<boolean>;
39
+ denied: Ref<boolean>;
40
+ unreachable: Ref<boolean>;
41
+ pass(redirect: string): Promise<void>;
42
+ recheck(redirect: string): void;
43
+ }
44
+ export interface AuthGateOptions {
45
+ deadlineMs?: number;
46
+ }
47
+ /** 造一道闸门。⚠️ 是工厂不是单例:状态只属于挂着它的那个外壳。 */
48
+ export declare function createAuthGate(options?: AuthGateOptions): AuthGate;
49
+ export declare function useAuth(): {
50
+ user: Ref<AuthUser | null, AuthUser | null>;
51
+ isStaff: import("vue").ComputedRef<boolean>;
52
+ /** 🔴 只是界面提示位(本地读得到 CSRF cookie 就算),**不是授权判据**:要挡就在后端挡。 */
53
+ isAuthenticated: () => boolean;
54
+ refresh: () => Promise<AuthUser | null>;
55
+ apiFetch: typeof apiFetch;
56
+ authHeaders: typeof authHeaders;
57
+ currentPath: typeof currentPath;
58
+ login: typeof loginWithRedirect;
59
+ logout: typeof logout;
60
+ };
@@ -0,0 +1 @@
1
+ export * from './auth-real';
@@ -0,0 +1,2 @@
1
+ export * from './auth';
2
+ export * from './ui';
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ /** 🔴 `uuid` 是前端写死的:后端按它找那一行(库里还没有也能先渲染成「未开发」)。`width` / `height` 数字按 rem 处理、字符串原样当 CSS 值。 */
2
+ type __VLS_Props = {
3
+ uuid: string;
4
+ name?: string;
5
+ module?: string;
6
+ width?: string | number;
7
+ height?: string | number;
8
+ flexible?: boolean;
9
+ fullSize?: boolean;
10
+ responsive?: boolean;
11
+ theme?: boolean;
12
+ };
13
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
14
+ name: string;
15
+ module: string;
16
+ flexible: boolean;
17
+ fullSize: boolean;
18
+ responsive: boolean;
19
+ theme: boolean;
20
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * full-size 的块**不许自己出竖滚动条**(滚的是外层页面,见 `MagicBlockHost.vue` 的 `fullSize`)。
3
+ *
4
+ * 🔴 这条要**在文档里**禁,不是在 iframe 标签上:`scrolling="no"` 与父页给的 `overflow: hidden`
5
+ * 都管不到文档自己的视口。宿主这一侧看不见块里的 DOM(沙箱是不透明源),所以只能随预览一起注入。
6
+ * 🔴 为什么不能靠"把宿主那层设得够高"就算了:块一出现滚动条,**内容宽度就少十几像素** ⇒
7
+ * 文字重新折行 ⇒ 量到的内容高度又变 ⇒ 宿主再改高度 —— 两边来回抖。
8
+ * `overflow: hidden` 把这个正反馈从源头掐掉(内容高度只由宽度决定,而宽度不再变)。
9
+ * ⚠️ 代价:块真比宿主给的位置更高时,多出来的部分**是被裁掉的**(不是藏起来还能滚)。
10
+ * 所以只在 full-size 这条路上加(`MagicBlockHost.vue` 的 `extras`),弹性那条路块该滚还得滚。
11
+ */
12
+ export declare const BLOCK_NO_SCROLL_CSS = "html, body { overflow: hidden !important; }";
13
+ /** 注入到块 iframe 里的 JS 源码(块没有构建步骤,它必须自己就是能跑的 JS)。
14
+ * 🔴 这是模板字符串:段内的注释出现反引号会提前截断它(实现也一律用字符串拼接)。 */
15
+ export declare const CLIENT_SOURCE = "\n(function () {\n var seq = 0, waiting = {}, queue = [], ready = false, last = -1;\n function call(method, args) {\n return new Promise(function (resolve, reject) {\n function run() {\n var id = ++seq;\n waiting[id] = { resolve: resolve, reject: reject };\n parent.postMessage({ '__mb': 'call', id: id, method: method, args: args }, '*');\n }\n if (ready) run(); else queue.push(run);\n });\n }\n window.addEventListener('message', function (event) {\n var data = event.data, pending;\n if (!data || data['__mb'] === undefined) return;\n if (data['__mb'] === 'host-ready') { ready = true; queue.splice(0).forEach(function (run) { run(); }); return; }\n if (data['__mb'] !== 'reply') return;\n if (!(pending = waiting[data.id])) return;\n delete waiting[data.id];\n if (data.ok) pending.resolve(data.value); else pending.reject(new Error(data.error || '\u8C03\u7528\u5931\u8D25'));\n });\n window.magicBlock = {\n getMe: function () { return call('getMe', []); },\n navigateTab: function (path) { return call('navigateTab', [path]); }\n };\n function make(prefix) {\n return new Proxy(function () {}, {\n get: function (_, name) { return make(prefix ? prefix + '.' + name : name); },\n apply: function (_, __, args) {\n var params = args[0] === undefined || args[0] === null ? {} : args[0];\n if (typeof params !== 'object' || Array.isArray(params)) return Promise.reject(new Error('magicBackendApi.' + prefix + '(params) \u7684\u53C2\u6570\u8981\u4E48\u662F\u4E00\u4E2A\u5BF9\u8C61\uFF08\u62FF\u4E0D\u51C6\u5C31\u4F20 {}\uFF09\uFF0C\u8981\u4E48\u662F\u4E00\u4E2A\u6587\u4EF6\uFF08File / Blob\uFF09'));\n return call('backend', [prefix, params]);\n }\n });\n }\n window.magicBackendApi = make('');\n if (typeof document === 'undefined') return;\n function report() {\n var body = document.body;\n if (!body) return;\n var max = -1, style = window.getComputedStyle(body), bodyTop = body.getBoundingClientRect().top;\n var gap = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);\n try {\n for (var i = 0; i < body.children.length; i += 1) {\n var el = body.children[i], cs = window.getComputedStyle(el), rect = el.getBoundingClientRect();\n if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed' || rect.height <= 0) continue;\n if (rect.bottom > max) max = rect.bottom;\n }\n } catch (error) { return; }\n var height = max < 0 ? 0 : Math.ceil(max - bodyTop + gap); /* \u4E00\u6761\u6B63\u5E38\u6D41\u5185\u5BB9\u90FD\u6CA1\u6709 \u21D2 \u62A5 0\uFF08\u5BBF\u4E3B\u6309\u8FD8\u6CA1\u91CF\u5230\u5904\u7406\uFF09 */\n /* \uD83D\uDD34 +1 \u662F\"\u53D6\u6574\u7684\u4F59\u91CF\"\uFF1A\u51E0\u4F55\u4E0A\u7B97\u51FA\u6765\u7684\u5185\u5BB9\u9AD8\u5EA6\u5E38\u5E38\u5DEE\u90A3\u4E48\u96F6\u70B9\u51E0\u50CF\u7D20\uFF08\u5B50\u50CF\u7D20\u6392\u7248\uFF09\uFF0C\n * \u5BBF\u4E3B\u7167\u7740\u5B83\u8BBE\u9AD8\u5EA6\u65F6\u5C31\u4F1A**\u5DEE\u4E00\u50CF\u7D20**\u2014\u2014\u90A3\u4E00\u50CF\u7D20\u6B63\u662F\u4E00\u4E2A\u7AD6\u6EDA\u52A8\u6761\u7684\u5F00\u5173\u3002 */\n if (height > 0) height += 1;\n if (height === last) return; /* \u4E00\u6837\u9AD8\u5C31\u4E0D\u518D\u53D1\uFF1A\u5426\u5219\u300C\u5BBF\u4E3B\u6539\u9AD8\u5EA6 \u2192 \u89E6\u53D1\u89C2\u5BDF \u2192 \u53C8\u53D1\u4E00\u6B21\u300D\u4F1A\u8F6C\u4E2A\u4E0D\u505C */\n last = height;\n parent.postMessage({ '__mb': 'height', height: height }, '*');\n }\n function start() {\n report();\n if (window.ResizeObserver && document.body) new ResizeObserver(report).observe(document.body);\n }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();\n setInterval(report, 500);\n})();\n";
16
+ export interface BridgeDeps {
17
+ uuid: string;
18
+ onHeight?: (height: number) => void;
19
+ /** 🔴 路由白名单在宿主手上(块看不到路由表)。 */
20
+ onNavigateTab?: (path: string) => Promise<string> | string;
21
+ }
22
+ export declare function installBridge(iframe: HTMLIFrameElement, deps: BridgeDeps): () => void;
@@ -0,0 +1,15 @@
1
+ /** 生成物在开发目录顶层的那两个名字。⚠️ 与后端 `export.DEV_PAGES_LOCK` **逐字一致**。 */
2
+ export declare const PAGES_LOCK = "pages.lock.ts";
3
+ /** 一条页面(从路由表现算出来的那一份:见 `AppSearch` 的 `collect()`,判据与它同一套)。 */
4
+ export interface PageEntry {
5
+ /** 路由路径(`navigateTab()` 收的就是它)。 */
6
+ path: string;
7
+ /** 页面的中文名(`meta.title`,没有就退到 `meta.sub` / 路径)。 */
8
+ title: string;
9
+ /** 分组(`meta.group`;没有就是空串)。 */
10
+ group: string;
11
+ }
12
+ /** 路径 → 枚举键:`/orders` ⇒ `Orders`,`/order-detail` ⇒ `OrderDetail`(中文路径退到 `Page2` 这种)。 */
13
+ export declare function pageKey(path: string, index: number): string;
14
+ /** 生成 `pages.lock.ts` 的正文(纯文本,写盘由调用方做)。 */
15
+ export declare function pagesLockTs(pages: PageEntry[]): string;
@@ -0,0 +1,34 @@
1
+ /** 亮红框的默认时长(毫秒):够人从一页跳到另一页之后还看得见那一下。 */
2
+ export declare const HIGHLIGHT_MS = 4000;
3
+ /** 订阅"高亮变了"(组件用;返回退订函数)。 */
4
+ export declare function onBlockHighlight(fn: () => void): () => void;
5
+ /** 现在该亮红框的是哪一块(空串 = 没有)。 */
6
+ export declare function highlightedUuid(): string;
7
+ /**
8
+ * 记下"这个块挂在这一页",返回退订函数(宿主卸载时调)。
9
+ *
10
+ * ⚠️ 同一个块挂在两页 ⇒ 两条都在;退订只去掉**这一页**那一条。
11
+ * ⚠️ uuid / 路径是空的一律不记(记了会变成"能跳到一页没有路径的地方")。
12
+ */
13
+ export declare function mountBlock(uuid: string, path: string): () => void;
14
+ /** 这个块还挂在哪一页(**除了 `currentPath`**);没有就回 `null`。 */
15
+ export declare function whereElse(uuid: string, currentPath: string): string | null;
16
+ /** 这个块记录到的全部页面(按记下来的顺序)。 */
17
+ export declare function mountedAt(uuid: string): string[];
18
+ /**
19
+ * 「定位这一块」该跳到哪一页(`null` = **不用跳**)。
20
+ *
21
+ * 🔴 与 `whereElse()` 是两件事、两套取舍,别混着用:
22
+ * · `whereElse()`(块自己那一圈被点)—— 优先去**别的**页面("这个块还挂在别处");
23
+ * · `locateTarget()`(控制台里点那一行)—— 本页就有就**留在本页**(就在眼皮底下,
24
+ * 把人传送走只会让人找不到刚点的那一块),本页没有才去第一个挂着它的页面。
25
+ */
26
+ export declare function locateTarget(uuid: string, currentPath: string): string | null;
27
+ /**
28
+ * 让这一块亮几秒红框(点那一圈时调)。
29
+ *
30
+ * 🔴 **先亮、再跳**:目标页那一块一露面就已经是"该亮"的状态,不必等它挂载之后再通知一次。
31
+ */
32
+ export declare function flashBlock(uuid: string, ms?: number): void;
33
+ /** 立刻收起高亮(用例、以及"不想再亮"的场合用)。 */
34
+ export declare function clearHighlight(): void;
@@ -0,0 +1,126 @@
1
+ /** 用户选中的那个目录(句柄留在内存里:刷新一次就要重选,**句柄存不进 localStorage**)。 */
2
+ export interface LocalDir {
3
+ /** 目录名(只用来显示)。 */
4
+ name: string;
5
+ handle: FileSystemDirectoryHandle;
6
+ }
7
+ /** 后端给的扫描规则(`export.scan_rules()` 的正文,字段名逐字对齐)。 */
8
+ export interface ScanRules {
9
+ /** 块包入口的名字(`index.open.html`)。 */
10
+ entry: string;
11
+ /** 代码铺在哪一层(导出铺的是 `workspace/`):导出前问"要不要覆盖"时用它认"这里已经有块了"。 */
12
+ blocksDir: string;
13
+ /** 认得的文本扩展名(其余一律跳过并报一条)。 */
14
+ textExt: string[];
15
+ /** 爬目录时跳过的目录名(依赖 / 构建产物 / 缓存)。 */
16
+ skipDirs: string[];
17
+ /** 平台生成物:**静默跳过**(不进库)。 */
18
+ platformFiles: string[];
19
+ /** 开发目录**顶层**直接忽略的文件名。 */
20
+ rootSkipFiles: string[];
21
+ /** 最多往下几层。 */
22
+ maxDepth: number;
23
+ /** 单个文件的上限(字节)。 */
24
+ maxFileBytes: number;
25
+ /** 一次"读回改动"的正文上限(字节):读到的合计超过它就当场说清,别让后端回一句看不懂的错。 */
26
+ maxBodyBytes: number;
27
+ }
28
+ /** 浏览器扫出来的一条失败(形状与后端 `export._failure()` 一致:`{kind, where, message}`)。 */
29
+ export interface ScanFailure {
30
+ kind: 'dir' | 'file';
31
+ where: string;
32
+ message: string;
33
+ }
34
+ /** 递给 `POST /magic-blocks/read` 的那一份扫描结果(**这就是"事实"**,后端据此配对与比对)。 */
35
+ export interface ScanUpload {
36
+ /** 选中的目录名(回话里原样带回来)。 */
37
+ root: string;
38
+ /** 候选块文件夹:`where` 是相对选中目录的路径,`files` 是包内相对路径 → 正文。 */
39
+ folders: {
40
+ where: string;
41
+ files: Record<string, string>;
42
+ }[];
43
+ failures: ScanFailure[];
44
+ /** 因为层数上限没走完 ⇒ 不能说"目录里没有就是删了"。 */
45
+ truncated: boolean;
46
+ }
47
+ /** 这个浏览器能不能直接写本机目录(Chrome / Edge 才有;还要 https 或 localhost)。 */
48
+ export declare function pickerAvailable(): boolean;
49
+ /** 弹系统目录选择框,回用户选的那个目录(取消 ⇒ 抛一句人话,不静默返回空)。 */
50
+ export declare function pickDirectory(): Promise<LocalDir>;
51
+ /**
52
+ * 路径 → 能逐级建目录的段(**在客户端再拦一道**)。
53
+ *
54
+ * 🔴 键是后端算出来的(它只铺 `workspace/…` 这种相对路径),这里再拦一道的理由是"落盘的判据不该只有上游":
55
+ * `..` / 绝对路径 / 盘符 / 反斜杠在**用户的盘上**是"写到别处去",而这一层是最后一道。
56
+ */
57
+ export declare function pathSegments(path: string): string[];
58
+ /** 一条写失败的记录(`path` 是包内相对路径,`message` 是一句人话)。 */
59
+ export interface WriteFailure {
60
+ path: string;
61
+ message: string;
62
+ }
63
+ /** `writePackage` 的结果。🔴 一个文件失败**不算整批失败**:写成的照写,失败的如实列出来。 */
64
+ export interface WriteResult {
65
+ written: number;
66
+ failed: WriteFailure[];
67
+ /** 一共排了几个文件(= `written + failed.length`,进度条的分母)。 */
68
+ total: number;
69
+ }
70
+ /**
71
+ * 把一整个开发包写进用户选的目录(`{相对路径: 正文}`)。
72
+ *
73
+ * 🔴 **清空是"问过才做"的动作**:导出前会先看目录里有什么、由控制台问一句(清空 / 只覆盖 / 取消)。
74
+ * 这个函数自己**不删任何东西**(`clearChildren()` 才是删那一步),它只管写。
75
+ * 🔴 **并发写 + 目录句柄缓存**:原来是一个文件一个文件地等、每个文件都从根重新走一遍目录,
76
+ * 几十个文件(含 540 KB 的主题那份)要串行好几趟目录树。
77
+ * 🔴 **一个文件写失败不许把整批吞掉**:失败收进 `failed` 回给调用方(这一层的判据与后端
78
+ * `export.write_dir` 一致 —— "一个块失败只报它自己"),调用方把它们显示出来。
79
+ * 原来是一抛全停:排在前面的文件已经落盘了,用户却只看到一句"失败",不知道铺了多少。
80
+ */
81
+ export declare function writePackage(dir: LocalDir, files: Record<string, string>, onProgress?: (done: number, total: number) => void): Promise<WriteResult>;
82
+ /** 选中目录里都有什么(顶层项 + 递归数出来的文件 / 目录数)—— 导出前"要不要清空"那个确认框用。 */
83
+ export interface DirInventory {
84
+ /** 顶层项(名字 + 是不是目录)。 */
85
+ items: {
86
+ name: string;
87
+ dir: boolean;
88
+ }[];
89
+ /** 顶层项个数。 */
90
+ topCount: number;
91
+ /** 递归数出来的文件数。 */
92
+ files: number;
93
+ /** 递归数出来的目录数(不含选中目录自己)。 */
94
+ dirs: number;
95
+ }
96
+ /**
97
+ * 数一遍选中目录里有什么(**只读**,不动任何东西)。
98
+ *
99
+ * 🔴 只走 `rules.maxDepth` 层:确认框要的是"大概有多少东西",为此把一个巨大的目录爬到底
100
+ * (依赖目录动辄几万个文件)只会让"导出"按钮按下去半天没反应。
101
+ */
102
+ export declare function inspectDirectory(dir: LocalDir, rules: ScanRules): Promise<DirInventory>;
103
+ /**
104
+ * 把选中目录**里面的东西**全删掉(选中目录本身不动,它外面的东西更不碰)。
105
+ *
106
+ * 🔴 判据只有一条:**只删 `dir` 的子项**。用 `handle.removeEntry(name, { recursive: true })` 按名字删,
107
+ * 路径**拼不出来**(不是"拼一个路径再删")—— 这样"删到别处去"在实现上就不可能发生。
108
+ * 🔴 删除是**不可逆**的,所以它只能由用户在确认框里点过"清空并导出"之后走到这里(见控制台)。
109
+ * ⚠️ 删完再铺:并发写的时候如果和删除同时跑,`getDirectoryHandle(create: true)` 可能建出马上又被删掉的目录。
110
+ */
111
+ export declare function clearChildren(dir: LocalDir): Promise<number>;
112
+ /**
113
+ * 选中的目录**本身**就是代码那一层时当场说清(`workspace/workspace/…` 是没人想要的)。
114
+ *
115
+ * 🔴 这是个很容易犯的错:目录选择框上写着"选一个文件夹",而 `workspace/` 也是一个文件夹。
116
+ * 选错一层的**后果**是代码铺进两层 `workspace/` 里,而"读回改动"照样认得出来(它按 uuid 认)——
117
+ * 于是这个错会一直躺着,直到有人去看目录结构。
118
+ */
119
+ export declare function assertNotBlocksDir(dir: LocalDir, rules: ScanRules): void;
120
+ /**
121
+ * 扫一遍用户选中的目录,攒出递给 `POST /magic-blocks/read` 的那一份事实。
122
+ *
123
+ * 🔴 超过正文上限**当场说清**:后端那边回的是"请求正文太大",而用户看到那句话时并不知道
124
+ * 自己能做什么(这一句里带着"多大 / 上限多少")。
125
+ */
126
+ export declare function scanDirectory(dir: LocalDir, rules: ScanRules): Promise<ScanUpload>;
@@ -0,0 +1,64 @@
1
+ /** 块包入口(必备件)。与后端 `sdk.BLOCK_ENTRY` 逐字一致。 */
2
+ export declare const BLOCK_ENTRY = "index.open.html";
3
+ /** 块包清单(必备件)。与后端 `sdk.BLOCK_MANIFEST` 逐字一致。 */
4
+ export declare const BLOCK_MANIFEST = "magic-block.open.yaml";
5
+ /** 清单只认这四个键,全部必填(顺序就是文档里的顺序)。 */
6
+ export declare const MANIFEST_KEYS: readonly ["id", "module", "name", "brief"];
7
+ export type ManifestKey = (typeof MANIFEST_KEYS)[number];
8
+ /** 可读名的字符上限(与后端 `NAME_MAX` 同一份)。 */
9
+ export declare const NAME_MAX = 40;
10
+ /** 包内文件名的形状。
11
+ * 🔴 `(?![A-Za-z]:)` 与 `(?!/)` **两个前瞻缺一不可**:后端 `FILE_NAME_PATTERN` 是
12
+ * `^(?!/)(?![A-Za-z]:)[A-Za-z0-9._\-/]+$`,而 `/etc/passwd` 是**字符合法但语义是绝对路径** ——
13
+ * 少一个前瞻就等于放行绝对路径(这一条是差分比对抓出来的,见 `tools/manifest-parity.mjs`)。 */
14
+ export declare const FILE_NAME_PATTERN: RegExp;
15
+ /** 清单里 `id` 的形状。 */
16
+ export declare const BLOCK_ID_PATTERN: RegExp;
17
+ /** 清单里 `module` 的形状。 */
18
+ export declare const MODULE_PATTERN: RegExp;
19
+ /** 清单里的键名形状。 */
20
+ export declare const MANIFEST_KEY_PATTERN: RegExp;
21
+ /** 清单里 `name` 的形状(🔴 这里**不能只判长度**:`' '` 长度合法但它是空的)。 */
22
+ export declare function validName(value: string): boolean;
23
+ /** 清单解析出来的字段(缺的 / 空的**不会**出现在这里)。 */
24
+ export type Manifest = Partial<Record<ManifestKey, string>>;
25
+ /** 一个块包的校验结果。`files` 是提交时**原样回给后端**的那一份,这里只读不写。 */
26
+ export interface BlockValidation {
27
+ /** 清单里读出来的四个键(没读全就是空对象)。 */
28
+ manifest: Manifest;
29
+ /** 逐条问题,中文、可直接显示给用户。 */
30
+ issues: string[];
31
+ }
32
+ /**
33
+ * 解析清单正文:`[认得的字段, 逐条问题]`(与后端 `parse_manifest` 同一套判据、同一套报文)。
34
+ *
35
+ * 🔴 不认识的写法(列表 / 嵌套 / 没有冒号 / 不认识的键 / 重复键)**各报一条**,绝不静默忽略 ——
36
+ * 静默的后果是"前端放行、后端拒绝",用户看到的是两套说法。
37
+ */
38
+ export declare function parseManifest(text: string): [Manifest, string[]];
39
+ /** 包内文件名合法吗(形状 + 不许 `..` 段)。 */
40
+ export declare function validFileName(name: string): boolean;
41
+ /**
42
+ * 一整个块包的判据(**前端这一半**):必备件 + 清单 + 文件名。
43
+ *
44
+ * 🔴 **不判块后端**:`backend_open.py` 要 `ast` 静态判据 + 子进程试加载,那是服务端才做得到的事
45
+ * (见 `validate.check_backend`)。这份函数过了不等于能存,只等于"值得发这一趟请求"。
46
+ */
47
+ export declare function validateBlock(files: Record<string, string>): BlockValidation;
48
+ /** 一次"保存选中的块"该带的全部信息。 */
49
+ export interface SelectionCheck {
50
+ /** 勾了几个块(0 = 一个都没勾)。 */
51
+ selected: number;
52
+ /** 所有问题(每条都带块名,直接显示)。 */
53
+ issues: string[];
54
+ }
55
+ /**
56
+ * 「保存选中的块」按钮的判据:勾了没有、每包过不过。
57
+ *
58
+ * 🔴 **一个块都没勾 = 直接拦下**:空请求打过去只会得到一句"没有能保存的块",
59
+ * 而用户真正的问题是"我忘了勾" —— 这件事只有浏览器知道。
60
+ */
61
+ export declare function checkSelection(items: {
62
+ name?: string;
63
+ files?: Record<string, string>;
64
+ }[]): SelectionCheck;
@@ -0,0 +1,11 @@
1
+ export declare const BLOCK_ENTRY = "index.open.html";
2
+ export declare const BLOCK_BACKEND_FILE = "backend_open.py";
3
+ export interface PreviewExtras {
4
+ /** 注入给块的引导脚本(`CLIENT_SOURCE`)。 */
5
+ bootstrap?: string;
6
+ /** 宿主额外加的 CSS:禁掉块自己的滚动条(`fullSize` 用)。 */
7
+ css?: string;
8
+ /** 应用主题 / 控件那一层 CSS(`theme.ts` 的 `collectThemeCss()`;由 `MagicBlockHost` 的 `theme` prop 决定给不给)。 */
9
+ themeCss?: string;
10
+ }
11
+ export declare function buildPreview(files: Record<string, string>, extras?: string | PreviewExtras): string;
@@ -0,0 +1,7 @@
1
+ export { default as MagicBlockHost } from './magic-block-MagicBlockHost.vue';
2
+ export { default as MagicBlockConsole } from './magic-block-MagicBlockConsole.vue';
3
+ export { loadBlockRender, onBlockRenderRefresh, refreshBlockRender, type BlockRender } from './magic-block-render';
4
+ export { BLOCK_BACKEND_FILE, BLOCK_ENTRY, buildPreview } from './magic-block-preview';
5
+ export { CLIENT_SOURCE, installBridge, type BridgeDeps } from './magic-block-bridge';
6
+ export { BLOCK_MANIFEST, MANIFEST_KEYS, NAME_MAX, checkSelection, parseManifest, validFileName, validName, validateBlock, type BlockValidation, type Manifest, type ManifestKey, type SelectionCheck, } from './magic-block-manifest';
7
+ export { API_BASE } from './auth-real';
@@ -0,0 +1,12 @@
1
+ export interface BlockRender {
2
+ id: string;
3
+ state: 'draft' | 'invalid' | 'ok';
4
+ name: string;
5
+ brief: string;
6
+ reason: string | null;
7
+ files?: Record<string, string>;
8
+ }
9
+ export declare function onBlockRenderRefresh(fn: () => void): () => void;
10
+ export declare function refreshBlockRender(): void;
11
+ /** 批量问状态(**只问还没问到过的那些**)。🔴 读不到(断网 / 会话失效)**不抛给宿主**:页面退回「按手上有什么显示」。 */
12
+ export declare function loadBlockRender(ids: string[]): Promise<BlockRender[]>;
@@ -0,0 +1,33 @@
1
+ /** 开发包根上那份主题 CSS 的名字。⚠️ 与后端 `devkit.THEME` **逐字一致**。 */
2
+ export declare const THEME_FILE = "labull.css";
3
+ /** 控件清单(照着抄 markup)。⚠️ 与后端 `devkit.CONTROLS` **逐字一致**。 */
4
+ export declare const CONTROLS_FILE = "CONTROLS.lock.html";
5
+ /** 收出来的那份样式(`css` 就是要写进文件里的正文)。 */
6
+ export interface ThemeBundle {
7
+ css: string;
8
+ /** 读到规则的样式表张数。 */
9
+ sheets: number;
10
+ /** 读不到规则的(跨源 / 还没加载完):如实列出来,别装作收全了。 */
11
+ skipped: string[];
12
+ /** 收了多少条规则。 */
13
+ rules: number;
14
+ }
15
+ /**
16
+ * 这条规则算不算"主题 / 控件"(其余一律丢掉:应用自己的业务样式不该混进开发包)。
17
+ *
18
+ * 留:`@font-face` / `@keyframes`;选择器里有 `.p-`(PrimeVue 控件)/ `.pi`(图标)/ `.labull-`(结构类)
19
+ * / `:root` / `html` / `body` / `*`;**`:where(...)` 开头的那一层**(主题给原生控件与 `<dialog>`
20
+ * 留的零特异度兜底 —— 它的声明里常常一个 token 都没有,例如 `::backdrop` 的纯色遮罩,
21
+ * 所以不能只靠"有没有 token"判它);或者这条规则里在定义 `--sp-*` / `--surface-*` 这类 token。
22
+ * 🔴 纯函数:只看文本,不看 DOM —— 所以能直接用 node 跑用例。
23
+ */
24
+ export declare function keepCssRule(selector: string, cssText: string): boolean;
25
+ /** 收一份自足的 CSS(规则从**当前页面已加载的样式表**里来)。 */
26
+ export declare function collectThemeCss(): ThemeBundle;
27
+ /**
28
+ * `CONTROLS.lock.html`:控件清单(一页能直接打开看的对照表,markup 可以照抄)。
29
+ *
30
+ * 🔴 只列**光有 CSS 就能看**的东西:PrimeVue 那些需要 JS 的组件(下拉、日期、弹窗)不在这里,
31
+ * 免得作者抄了 markup 却发现没有行为(那比不给他更糟)。
32
+ */
33
+ export declare function controlsPage(): string;
@@ -0,0 +1 @@
1
+ export * from './magic-block-real';
@@ -0,0 +1,3 @@
1
+ import '../../theme/tokens.css';
2
+ import '../../theme/labull.css';
3
+ export declare const THEME_ENTRY = "labull-framework/theme";
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,31 @@
1
+ import type { SideItem } from './ui-types';
2
+ type __VLS_Props = {
3
+ actions?: boolean;
4
+ side?: SideItem[];
5
+ logo?: string;
6
+ caption?: string;
7
+ /** 左侧图标栏最上面那个"首页"按钮的路径(默认 `/`)。配合 `side` 用。 */
8
+ home?: string;
9
+ /** 底部竖排的产品名(不给就不画)。 */
10
+ brand?: string;
11
+ };
12
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
13
+ type __VLS_Slots = {} & {
14
+ bar?: (props: typeof __VLS_1) => any;
15
+ } & {
16
+ appbar?: (props: typeof __VLS_3) => any;
17
+ } & {
18
+ actions?: (props: typeof __VLS_5) => any;
19
+ };
20
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
+ home: string;
22
+ actions: boolean;
23
+ caption: string;
24
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
26
+ export default _default;
27
+ type __VLS_WithSlots<T, S> = T & {
28
+ new (): {
29
+ $slots: S;
30
+ };
31
+ };
@@ -0,0 +1,14 @@
1
+ import type { SideItem } from './ui-types';
2
+ type __VLS_Props = {
3
+ items: SideItem[];
4
+ /** 底部竖排的产品名(不给就不画)。 */
5
+ brand?: string;
6
+ /** 图标栏最上面那一个"首页"按钮:点它跳这个路径(**不开新标签的例外**,它由路由自己记账)。 */
7
+ home?: string;
8
+ /** 首页按钮的名字。 */
9
+ homeTitle?: string;
10
+ /** 首页按钮的图标。 */
11
+ homeIcon?: string;
12
+ };
13
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ export interface SharedGate {
2
+ /** 认人;没登录时**只跳一次** Authing。返回"这次认出来了吗"。 */
3
+ pass(redirect: string): Promise<boolean>;
4
+ /** 身份作废(退出登录 / 从 bfcache 退回来):下一次问会重新认。 */
5
+ reset(): void;
6
+ }
7
+ /** 按地址栏认人(路由守卫用的就是这一条)。 */
8
+ export declare function passShared(): Promise<boolean>;
9
+ export declare function resetShared(): void;
@@ -0,0 +1,16 @@
1
+ import type { App, Plugin } from 'vue';
2
+ import AppSearch from './ui-AppSearch.vue';
3
+ import ProductShell from './ui-ProductShell.vue';
4
+ import ProductSide from './ui-ProductSide.vue';
5
+ import ProductTabs from './ui-ProductTabs.vue';
6
+ import UserMenu from './ui-UserMenu.vue';
7
+ export { AppSearch, ProductShell, ProductSide, ProductTabs, UserMenu };
8
+ export { openTab, closeTab, registerTabLabel, useTabs, TABS_MAX } from './ui-tabs';
9
+ export type { TabItem } from './ui-tabs';
10
+ export type { SideItem } from './ui-types';
11
+ export interface LabullOptions {
12
+ extra?: Plugin[];
13
+ }
14
+ export declare function installApp(app: App, options?: LabullOptions): void;
15
+ export declare const labull: Plugin;
16
+ export default labull;
@@ -0,0 +1,31 @@
1
+ /** 标签上限:点开太多页时丢最早那个(界面上的东西,不设上限会越攒越长)。 */
2
+ export declare const TABS_MAX = 12;
3
+ export interface TabItem {
4
+ /** 标签的键,就是路径。 */
5
+ key: string;
6
+ path: string;
7
+ title: string;
8
+ /** PrimeIcons 的类名,例如 `pi pi-home`。 */
9
+ icon: string;
10
+ }
11
+ /**
12
+ * 页面自报名字(可选)。
13
+ *
14
+ * 🔴 为什么需要它:`meta.title` 只对**写死在路由表里**的页面有效;带参数的页面(`/orders/42`)
15
+ * 要显示"订单 SO-1"这种名字,只有页面自己知道。页面在 `onMounted` 里调一次即可。
16
+ */
17
+ export declare function registerTabLabel(path: string, title: string, icon?: string): void;
18
+ /** 开一个标签(已经开着就切过去)并把当前页指到它 —— **这是"跳页"的唯一入口**。 */
19
+ export declare function openTab(path: string, meta?: {
20
+ title?: unknown;
21
+ icon?: unknown;
22
+ }): void;
23
+ /** 关一个标签,回"下一个该去哪"的路径(关掉的不是当前那个就回空串)。 */
24
+ export declare function closeTab(path: string): string;
25
+ /** 标签那一栏要读的东西。 */
26
+ export declare function useTabs(): {
27
+ tabs: import("vue").ComputedRef<TabItem[]>;
28
+ active: import("vue").ComputedRef<string>;
29
+ openTab: typeof openTab;
30
+ closeTab: typeof closeTab;
31
+ };
@@ -0,0 +1,20 @@
1
+ /** 侧栏菜单的形状。
2
+ *
3
+ * 🔴 **只有两级**(与老项目一致):
4
+ * · 一级 = **分组**:只负责展开 / 收起,**本身不是页面、点它不跳转、也不开标签**;
5
+ * · 二级 = **页面**:点它才跳转并**开一个标签**。
6
+ * 所以下面 `children` 里的每一项都必须有 `path`(一级那个 `path` 只是这一组的标识,页面用不到它)。
7
+ *
8
+ * ⚠️ 想从路由表现算这份名单也行(老项目的 `pages.lock.ts` 就是那么做的:按 `meta.group` 分组、
9
+ * 二级取 `meta.title` / `meta.sub`),但**那是使用者的事** —— 库只认这张表。
10
+ */
11
+ export interface SideItem {
12
+ /** 一级分组的标识(**不跳转**,只用来认这一组)。 */
13
+ path: string;
14
+ /** 分组名(一级)。 */
15
+ title: string;
16
+ /** 分组图标(PrimeIcons 类名,例如 `pi pi-home`)。 */
17
+ icon?: string;
18
+ /** 这一组下面的页面(二级)—— 点它才跳转并开标签。 */
19
+ children?: SideItem[];
20
+ }
@@ -0,0 +1 @@
1
+ export * from './ui-real';
package/dist/ui.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './types/ui'
package/dist/ui.js ADDED
@@ -0,0 +1,15 @@
1
+ import { A as e, P as l, a as r, b, T as o, U as u, c as T, i as c, l as p, o as t, r as A, u as d } from "./chunk-index-_jasBCim.js";
2
+ export {
3
+ e as AppSearch,
4
+ l as ProductShell,
5
+ r as ProductSide,
6
+ b as ProductTabs,
7
+ o as TABS_MAX,
8
+ u as UserMenu,
9
+ T as closeTab,
10
+ c as installApp,
11
+ p as labull,
12
+ t as openTab,
13
+ A as registerTabLabel,
14
+ d as useTabs
15
+ };