@ssobig/writer-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/asset-repository.js +278 -0
- package/config.js +14 -0
- package/package.json +28 -0
- package/project-runtime.js +102 -0
- package/storage-path.js +110 -0
- package/templates/mystery-v1/authoring-view-preference.js +34 -0
- package/templates/mystery-v1/character-perspective-preview.js +61 -0
- package/templates/mystery-v1/component-asset-operations.js +103 -0
- package/templates/mystery-v1/component-autosave.js +121 -0
- package/templates/mystery-v1/component-catalog-contract.js +340 -0
- package/templates/mystery-v1/component-checkpoint-history.js +145 -0
- package/templates/mystery-v1/component-contract.js +90 -0
- package/templates/mystery-v1/component-draft-operations.js +313 -0
- package/templates/mystery-v1/component-field-contracts.js +595 -0
- package/templates/mystery-v1/component-id-policy.js +64 -0
- package/templates/mystery-v1/component-manager.js +396 -0
- package/templates/mystery-v1/component-navigation-counts.js +64 -0
- package/templates/mystery-v1/component-registry.js +205 -0
- package/templates/mystery-v1/component-renderers.js +139 -0
- package/templates/mystery-v1/component-storage-contract.js +237 -0
- package/templates/mystery-v1/external-update-coordinator.js +91 -0
- package/templates/mystery-v1/output-clue-card-layout.js +46 -0
- package/templates/mystery-v1/page-header.js +26 -0
- package/templates/mystery-v1/render-ui-state.js +76 -0
- package/templates/mystery-v1/runtime-snapshot-reconciler.js +40 -0
- package/templates/mystery-v1/tab-bar.js +87 -0
- package/templates/mystery-v1/view-component-contract.js +152 -0
- package/templates/mystery-v1/view-component-registry.js +44 -0
- package/templates/mystery-v1/view-component-runtime.js +95 -0
- package/tools/writer-cli/bin/ssobig-writer-daemon.cjs +34 -0
- package/tools/writer-cli/bin/ssobig-writer.cjs +12 -0
- package/tools/writer-cli/package-lock.json +121 -0
- package/tools/writer-cli/package.json +22 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +38 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/agents/openai.yaml +4 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +10 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +5 -0
- package/tools/writer-cli/src/agent-paths.cjs +114 -0
- package/tools/writer-cli/src/agent-service.cjs +496 -0
- package/tools/writer-cli/src/asset-policy.cjs +113 -0
- package/tools/writer-cli/src/auth.cjs +655 -0
- package/tools/writer-cli/src/checkpoint-diff.cjs +128 -0
- package/tools/writer-cli/src/command-registry.cjs +152 -0
- package/tools/writer-cli/src/commands.cjs +841 -0
- package/tools/writer-cli/src/corpus.cjs +83 -0
- package/tools/writer-cli/src/daemon-app.cjs +106 -0
- package/tools/writer-cli/src/daemon-client.cjs +187 -0
- package/tools/writer-cli/src/daemon-protocol.cjs +184 -0
- package/tools/writer-cli/src/daemon-runner.cjs +97 -0
- package/tools/writer-cli/src/daemon-server.cjs +378 -0
- package/tools/writer-cli/src/diagnostics.cjs +235 -0
- package/tools/writer-cli/src/domain.cjs +731 -0
- package/tools/writer-cli/src/errors.cjs +47 -0
- package/tools/writer-cli/src/gateway.cjs +357 -0
- package/tools/writer-cli/src/investigation-board-layout.cjs +328 -0
- package/tools/writer-cli/src/json-patch.cjs +98 -0
- package/tools/writer-cli/src/json.cjs +26 -0
- package/tools/writer-cli/src/local-index-cache.cjs +139 -0
- package/tools/writer-cli/src/local-index-lookup.cjs +98 -0
- package/tools/writer-cli/src/local-index-query.cjs +304 -0
- package/tools/writer-cli/src/local-index-snapshot.cjs +235 -0
- package/tools/writer-cli/src/local-index-storage.cjs +284 -0
- package/tools/writer-cli/src/local-index.cjs +199 -0
- package/tools/writer-cli/src/mutations.cjs +722 -0
- package/tools/writer-cli/src/platform-runner.cjs +55 -0
- package/tools/writer-cli/src/project-import.cjs +485 -0
- package/tools/writer-cli/src/skill-manager.cjs +255 -0
- package/tools/writer-cli/src/source-fingerprint.cjs +90 -0
- package/tools/writer-cli/src/update-gate.cjs +102 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchExternalUpdateCoordinator = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
function create(options = {}) {
|
|
9
|
+
if (typeof options.drain !== "function" || typeof options.load !== "function" || typeof options.apply !== "function") {
|
|
10
|
+
throw new TypeError("외부 변경 coordinator에는 drain, load, apply 함수가 필요합니다.");
|
|
11
|
+
}
|
|
12
|
+
const schedule = options.setTimeout || setTimeout;
|
|
13
|
+
const cancel = options.clearTimeout || clearTimeout;
|
|
14
|
+
const delay = Number.isFinite(Number(options.delay)) ? Math.max(0, Number(options.delay)) : 120;
|
|
15
|
+
let timer = null;
|
|
16
|
+
let requested = false;
|
|
17
|
+
let running = null;
|
|
18
|
+
let disposed = false;
|
|
19
|
+
|
|
20
|
+
async function processRequests() {
|
|
21
|
+
if (running) return running;
|
|
22
|
+
running = (async () => {
|
|
23
|
+
let applied = false;
|
|
24
|
+
while (!disposed && requested) {
|
|
25
|
+
requested = false;
|
|
26
|
+
await options.drain();
|
|
27
|
+
const snapshot = await options.load();
|
|
28
|
+
if (options.isLocalBusy?.()) {
|
|
29
|
+
requested = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const changed = await options.apply(snapshot);
|
|
33
|
+
if (changed) {
|
|
34
|
+
applied = true;
|
|
35
|
+
options.onApplied?.(snapshot);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, applied };
|
|
39
|
+
})();
|
|
40
|
+
try {
|
|
41
|
+
return await running;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
options.onError?.(error);
|
|
44
|
+
return { ok: false, applied: false, error };
|
|
45
|
+
} finally {
|
|
46
|
+
running = null;
|
|
47
|
+
if (!disposed && requested && timer === null) {
|
|
48
|
+
timer = schedule(() => {
|
|
49
|
+
timer = null;
|
|
50
|
+
void processRequests();
|
|
51
|
+
}, delay);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function request() {
|
|
57
|
+
if (disposed) return false;
|
|
58
|
+
requested = true;
|
|
59
|
+
if (timer !== null || running) return true;
|
|
60
|
+
timer = schedule(() => {
|
|
61
|
+
timer = null;
|
|
62
|
+
void processRequests();
|
|
63
|
+
}, delay);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function flush() {
|
|
68
|
+
if (timer !== null) {
|
|
69
|
+
cancel(timer);
|
|
70
|
+
timer = null;
|
|
71
|
+
}
|
|
72
|
+
if (running) {
|
|
73
|
+
await running.catch(() => {});
|
|
74
|
+
if (!requested) return { ok: true, applied: false };
|
|
75
|
+
}
|
|
76
|
+
if (!requested || disposed) return { ok: true, applied: false };
|
|
77
|
+
return processRequests();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function dispose() {
|
|
81
|
+
disposed = true;
|
|
82
|
+
requested = false;
|
|
83
|
+
if (timer !== null) cancel(timer);
|
|
84
|
+
timer = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.freeze({ request, flush, dispose });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.freeze({ create });
|
|
91
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
root.WriterWorkbenchClueCardLayout = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : window, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const ROWS = 3;
|
|
9
|
+
const COLUMNS = 3;
|
|
10
|
+
const CARDS_PER_SHEET = ROWS * COLUMNS;
|
|
11
|
+
|
|
12
|
+
function normalizeGrid(value) {
|
|
13
|
+
const rows = Number.isInteger(value?.rows) && value.rows > 0 ? value.rows : ROWS;
|
|
14
|
+
const columns = Number.isInteger(value?.columns) && value.columns > 0 ? value.columns : COLUMNS;
|
|
15
|
+
return { rows, columns, cardsPerSheet: rows * columns };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeFlip(value) {
|
|
19
|
+
return value === "short" ? "short" : "long";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mirrorSlots(values, flip = "long", grid) {
|
|
23
|
+
const { rows, columns, cardsPerSheet } = normalizeGrid(grid);
|
|
24
|
+
const slots = Array.from({ length: cardsPerSheet }, (_, index) => values?.[index] ?? null);
|
|
25
|
+
const normalizedFlip = normalizeFlip(flip);
|
|
26
|
+
return slots.map((_, slotIndex) => {
|
|
27
|
+
const row = Math.floor(slotIndex / columns);
|
|
28
|
+
const column = slotIndex % columns;
|
|
29
|
+
const sourceRow = normalizedFlip === "short" ? rows - 1 - row : row;
|
|
30
|
+
const sourceColumn = normalizedFlip === "long" ? columns - 1 - column : column;
|
|
31
|
+
return slots[sourceRow * columns + sourceColumn];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sheets(values, grid) {
|
|
36
|
+
const { cardsPerSheet } = normalizeGrid(grid);
|
|
37
|
+
const items = Array.isArray(values) ? values : [];
|
|
38
|
+
const result = [];
|
|
39
|
+
for (let index = 0; index < items.length; index += cardsPerSheet) {
|
|
40
|
+
result.push(Array.from({ length: cardsPerSheet }, (_, offset) => items[index + offset] ?? null));
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.freeze({ ROWS, COLUMNS, CARDS_PER_SHEET, normalizeGrid, normalizeFlip, mirrorSlots, sheets });
|
|
46
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchPageHeader = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
function escapeHtml(value) {
|
|
9
|
+
return String(value ?? "")
|
|
10
|
+
.replaceAll("&", "&")
|
|
11
|
+
.replaceAll("<", "<")
|
|
12
|
+
.replaceAll(">", ">")
|
|
13
|
+
.replaceAll('"', """)
|
|
14
|
+
.replaceAll("'", "'");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function render(options = {}) {
|
|
18
|
+
const title = escapeHtml(options.title || "");
|
|
19
|
+
const description = escapeHtml(options.description || "");
|
|
20
|
+
const className = ["page-header", options.className].filter(Boolean).join(" ");
|
|
21
|
+
const metaMarkup = String(options.metaMarkup || "");
|
|
22
|
+
return `<header class="${escapeHtml(className)}"><div class="page-header-copy"><h2>${title}</h2>${description ? `<p>${description}</p>` : ""}</div>${metaMarkup ? `<div class="page-header-meta">${metaMarkup}</div>` : ""}</header>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Object.freeze({ escapeHtml, render });
|
|
26
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchRenderUiState = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const IDENTITY_NAMES = Object.freeze([
|
|
9
|
+
"data-ui-scroll-key", "data-renderer-field", "data-component-json", "data-view-component-id", "data-preview-character-id", "data-component-id", "data-component-manager-tab",
|
|
10
|
+
"data-workspace-mode", "data-authoring-view", "data-component-manager-select", "data-progress-key"
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function locator(element, root) {
|
|
14
|
+
const identityName = IDENTITY_NAMES.find(name => element.hasAttribute?.(name));
|
|
15
|
+
if (identityName) {
|
|
16
|
+
const attributes = { [identityName]: element.getAttribute(identityName) };
|
|
17
|
+
if (element.hasAttribute("data-component-kind")) attributes["data-component-kind"] = element.getAttribute("data-component-kind");
|
|
18
|
+
return { type: "attributes", tag: element.tagName, attributes };
|
|
19
|
+
}
|
|
20
|
+
if (element.id) return { type: "id", id: element.id };
|
|
21
|
+
const classes = [...(element.classList || [])].sort();
|
|
22
|
+
const candidates = [...root.querySelectorAll(element.tagName)].filter(candidate => classes.every(name => candidate.classList.contains(name)));
|
|
23
|
+
return { type: "structure", tag: element.tagName, classes, index: Math.max(0, candidates.indexOf(element)) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function locate(root, target) {
|
|
27
|
+
if (!target) return null;
|
|
28
|
+
if (target.type === "id") return [...root.querySelectorAll("[id]")].find(element => element.id === target.id) || null;
|
|
29
|
+
if (target.type === "attributes") return [...root.querySelectorAll(target.tag)].find(element => Object.entries(target.attributes).every(([name, value]) => element.getAttribute(name) === value)) || null;
|
|
30
|
+
const candidates = [...root.querySelectorAll(target.tag)].filter(element => target.classes.every(name => element.classList.contains(name)));
|
|
31
|
+
return candidates[target.index] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function captureFocus(element, main) {
|
|
35
|
+
if (!element || !main.contains(element)) return null;
|
|
36
|
+
return {
|
|
37
|
+
locator: locator(element, main),
|
|
38
|
+
selectionStart: typeof element.selectionStart === "number" ? element.selectionStart : null,
|
|
39
|
+
selectionEnd: typeof element.selectionEnd === "number" ? element.selectionEnd : null,
|
|
40
|
+
selectionDirection: element.selectionDirection || "none"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function capture(main, activeElement, viewport) {
|
|
45
|
+
const scroll = [{ root: true, top: main.scrollTop, left: main.scrollLeft }];
|
|
46
|
+
main.querySelectorAll("*").forEach(element => {
|
|
47
|
+
if (element.scrollTop || element.scrollLeft) scroll.push({ locator: locator(element, main), top: element.scrollTop, left: element.scrollLeft });
|
|
48
|
+
});
|
|
49
|
+
const disclosures = [...main.querySelectorAll("details")].map(element => ({ locator: locator(element, main), open: element.open }));
|
|
50
|
+
return { viewport, scroll, focus: captureFocus(activeElement, main), disclosures };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function restore(main, state, options = {}) {
|
|
54
|
+
if (!state) return;
|
|
55
|
+
state.disclosures?.forEach(item => {
|
|
56
|
+
const element = locate(main, item.locator);
|
|
57
|
+
if (element) element.open = item.open;
|
|
58
|
+
});
|
|
59
|
+
state.scroll?.forEach(item => {
|
|
60
|
+
const element = item.root ? main : locate(main, item.locator);
|
|
61
|
+
if (!element) return;
|
|
62
|
+
element.scrollTop = item.top;
|
|
63
|
+
element.scrollLeft = item.left;
|
|
64
|
+
});
|
|
65
|
+
if (options.focus !== false && state.focus) {
|
|
66
|
+
const element = locate(main, state.focus.locator);
|
|
67
|
+
element?.focus?.({ preventScroll: true });
|
|
68
|
+
if (element && state.focus.selectionStart !== null && typeof element.setSelectionRange === "function") {
|
|
69
|
+
element.setSelectionRange(state.focus.selectionStart, state.focus.selectionEnd, state.focus.selectionDirection);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
options.scrollTo?.(state.viewport);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Object.freeze({ locator, locate, captureFocus, capture, restore });
|
|
76
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchRuntimeSnapshotReconciler = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
function validate(snapshot) {
|
|
9
|
+
if (!snapshot?.project || !Array.isArray(snapshot.dataComponents) || !Array.isArray(snapshot.viewComponents)) {
|
|
10
|
+
throw new Error("Runtime snapshot 계약이 완전하지 않습니다.");
|
|
11
|
+
}
|
|
12
|
+
return snapshot;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function updateRecord(current, next, clone) {
|
|
16
|
+
const replacement = clone(next);
|
|
17
|
+
Object.keys(current).forEach(key => { delete current[key]; });
|
|
18
|
+
Object.assign(current, replacement);
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function reconcile(snapshot, currentActiveComponents, clone) {
|
|
23
|
+
const next = validate(snapshot);
|
|
24
|
+
if (!Array.isArray(currentActiveComponents) || typeof clone !== "function") {
|
|
25
|
+
throw new Error("Runtime snapshot reconciliation 의존성이 완전하지 않습니다.");
|
|
26
|
+
}
|
|
27
|
+
const currentById = new Map(currentActiveComponents.map(instance => [instance.instanceId, instance]));
|
|
28
|
+
const activeComponents = next.dataComponents
|
|
29
|
+
.filter(instance => instance.enabled && !instance.archived)
|
|
30
|
+
.map(instance => {
|
|
31
|
+
const current = currentById.get(instance.instanceId);
|
|
32
|
+
return current ? updateRecord(current, instance, clone) : clone(instance);
|
|
33
|
+
});
|
|
34
|
+
const activeViewComponents = next.viewComponents.filter(instance => instance.enabled).map(instance => clone(instance));
|
|
35
|
+
const archivedComponents = (Array.isArray(next.archivedDataComponents) ? next.archivedDataComponents : []).map(instance => clone(instance));
|
|
36
|
+
return Object.freeze({ activeComponents, activeViewComponents, archivedComponents });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Object.freeze({ validate, updateRecord, reconcile });
|
|
40
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchTabBar = api;
|
|
5
|
+
})(typeof window !== "undefined" ? window : globalThis, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const VARIANTS = new Set(["primary", "secondary"]);
|
|
9
|
+
|
|
10
|
+
function normalizeCount(value) {
|
|
11
|
+
const count = Number(value);
|
|
12
|
+
return Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function bindKeyboardNavigation(container) {
|
|
16
|
+
container.onkeydown = event => {
|
|
17
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
18
|
+
const tabs = [...container.querySelectorAll('[role="tab"]')];
|
|
19
|
+
const currentIndex = tabs.indexOf(event.target.closest?.('[role="tab"]'));
|
|
20
|
+
if (currentIndex < 0 || tabs.length < 2) return;
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
let nextIndex = currentIndex;
|
|
23
|
+
if (event.key === "Home") nextIndex = 0;
|
|
24
|
+
else if (event.key === "End") nextIndex = tabs.length - 1;
|
|
25
|
+
else if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
26
|
+
else if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length;
|
|
27
|
+
tabs[nextIndex].focus();
|
|
28
|
+
tabs[nextIndex].click();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function revealActiveTab(container) {
|
|
33
|
+
const active = container.querySelector('[role="tab"][aria-selected="true"]');
|
|
34
|
+
if (!active) return;
|
|
35
|
+
const reveal = () => active.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
36
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(reveal);
|
|
37
|
+
else reveal();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function render(container, tabs, options = {}) {
|
|
41
|
+
if (!container || typeof container.replaceChildren !== "function") {
|
|
42
|
+
throw new TypeError("TabBar 컨테이너가 필요합니다.");
|
|
43
|
+
}
|
|
44
|
+
const variant = VARIANTS.has(options.variant) ? options.variant : "primary";
|
|
45
|
+
container.classList.add("tab-bar");
|
|
46
|
+
container.classList.toggle("tab-bar-primary", variant === "primary");
|
|
47
|
+
container.classList.toggle("tab-bar-secondary", variant === "secondary");
|
|
48
|
+
container.dataset.tabBarVariant = variant;
|
|
49
|
+
container.setAttribute("role", "tablist");
|
|
50
|
+
if (options.ariaLabel) container.setAttribute("aria-label", String(options.ariaLabel));
|
|
51
|
+
|
|
52
|
+
const fragment = document.createDocumentFragment();
|
|
53
|
+
(Array.isArray(tabs) ? tabs : []).forEach(tab => {
|
|
54
|
+
const button = document.createElement("button");
|
|
55
|
+
button.type = "button";
|
|
56
|
+
button.setAttribute("role", "tab");
|
|
57
|
+
button.setAttribute("aria-selected", String(Boolean(tab.active)));
|
|
58
|
+
button.setAttribute("aria-pressed", String(Boolean(tab.active)));
|
|
59
|
+
button.tabIndex = tab.active ? 0 : -1;
|
|
60
|
+
if (tab.ariaLabel) button.setAttribute("aria-label", String(tab.ariaLabel));
|
|
61
|
+
if (tab.active) button.classList.add("is-active");
|
|
62
|
+
Object.entries(tab.attributes || {}).forEach(([name, value]) => {
|
|
63
|
+
if (value !== undefined && value !== null) button.setAttribute(name, String(value));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const label = document.createElement("span");
|
|
67
|
+
label.textContent = String(tab.label || "");
|
|
68
|
+
button.append(label);
|
|
69
|
+
|
|
70
|
+
const count = normalizeCount(tab.count);
|
|
71
|
+
if (count) {
|
|
72
|
+
const badge = document.createElement("span");
|
|
73
|
+
badge.className = "nav-count";
|
|
74
|
+
badge.setAttribute("aria-hidden", "true");
|
|
75
|
+
badge.textContent = String(count);
|
|
76
|
+
button.append(badge);
|
|
77
|
+
}
|
|
78
|
+
fragment.append(button);
|
|
79
|
+
});
|
|
80
|
+
container.replaceChildren(fragment);
|
|
81
|
+
bindKeyboardNavigation(container);
|
|
82
|
+
revealActiveTab(container);
|
|
83
|
+
return container;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Object.freeze({ render });
|
|
87
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
let registryModule = root && root.WriterWorkbenchViewComponentRegistry;
|
|
3
|
+
if (typeof module === "object" && module.exports) registryModule = require("./view-component-registry.js");
|
|
4
|
+
const api = factory(registryModule);
|
|
5
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
6
|
+
if (root) root.WriterWorkbenchViewComponentContract = api;
|
|
7
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (registryModule) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
if (!registryModule) throw new Error("WriterWorkbenchViewComponentRegistry is required");
|
|
11
|
+
|
|
12
|
+
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
const TEMPLATE_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
14
|
+
const MODES = Object.freeze(["authoring", "preview", "validation"]);
|
|
15
|
+
const ACCESS_MODES = Object.freeze(["read", "read_write"]);
|
|
16
|
+
const REMOVED_VERSION_FIELDS = Object.freeze(["view_template_version", "templateVersion"]);
|
|
17
|
+
|
|
18
|
+
function clone(value) {
|
|
19
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
20
|
+
return JSON.parse(JSON.stringify(value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isObject(value) {
|
|
24
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positiveInteger(value) {
|
|
28
|
+
const number = Number(value);
|
|
29
|
+
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nonNegativeInteger(value) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function invalid(message) {
|
|
38
|
+
const error = new Error(message);
|
|
39
|
+
error.code = "SOMI_VIEW_COMPONENT_INVALID";
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeView(row, registry) {
|
|
44
|
+
if (!isObject(row)) invalid("View Component 행이 객체가 아닙니다.");
|
|
45
|
+
if (REMOVED_VERSION_FIELDS.some(field => Object.hasOwn(row, field))) invalid("View Component에 제거된 version 필드가 포함되어 있습니다.");
|
|
46
|
+
const viewInstanceId = String(row.view_instance_id || "");
|
|
47
|
+
const templateId = String(row.view_template_id || "");
|
|
48
|
+
const rendererId = String(row.renderer_id || "");
|
|
49
|
+
const label = String(row.label || "").trim();
|
|
50
|
+
const order = nonNegativeInteger(row.sort_order);
|
|
51
|
+
const revision = positiveInteger(row.revision);
|
|
52
|
+
const modes = Array.isArray(row.supported_modes) ? [...new Set(row.supported_modes.map(String))] : [];
|
|
53
|
+
if (!ID_PATTERN.test(viewInstanceId)) invalid(`View Component ID가 올바르지 않습니다: ${viewInstanceId || "(비어 있음)"}`);
|
|
54
|
+
if (!TEMPLATE_PATTERN.test(templateId)) invalid(`${viewInstanceId}의 View Template 식별자가 올바르지 않습니다.`);
|
|
55
|
+
if (!TEMPLATE_PATTERN.test(rendererId)) invalid(`${viewInstanceId}의 renderer_id가 올바르지 않습니다.`);
|
|
56
|
+
if (!label || label.length > 120 || order === null || revision === null) invalid(`${viewInstanceId}의 View Component 메타데이터가 올바르지 않습니다.`);
|
|
57
|
+
if (!modes.length || modes.some(mode => !MODES.includes(mode))) invalid(`${viewInstanceId}의 지원 Mode가 올바르지 않습니다.`);
|
|
58
|
+
if (!isObject(row.config)) invalid(`${viewInstanceId}의 View config는 객체여야 합니다.`);
|
|
59
|
+
const definition = registry.get(templateId);
|
|
60
|
+
if (!definition) invalid(`지원하지 않는 View Component입니다: ${templateId}`);
|
|
61
|
+
if (definition.rendererId !== rendererId) invalid(`${viewInstanceId}의 renderer_id가 View Template 계약과 다릅니다.`);
|
|
62
|
+
if (modes.length !== definition.supportedModes.length || modes.some(mode => !definition.supportedModes.includes(mode))) {
|
|
63
|
+
invalid(`${viewInstanceId}의 지원 Mode가 View Template 계약과 다릅니다.`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
viewInstanceId,
|
|
67
|
+
templateId,
|
|
68
|
+
label,
|
|
69
|
+
description: String(row.description || ""),
|
|
70
|
+
rendererId,
|
|
71
|
+
modes,
|
|
72
|
+
order,
|
|
73
|
+
enabled: row.is_enabled === true,
|
|
74
|
+
config: clone(row.config),
|
|
75
|
+
revision,
|
|
76
|
+
bindings: [],
|
|
77
|
+
definition
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeBinding(row) {
|
|
82
|
+
if (!isObject(row)) invalid("View Component binding 행이 객체가 아닙니다.");
|
|
83
|
+
const viewInstanceId = String(row.view_instance_id || "");
|
|
84
|
+
const key = String(row.binding_key || "");
|
|
85
|
+
const dataInstanceId = String(row.data_instance_id || "");
|
|
86
|
+
const accessMode = String(row.access_mode || "");
|
|
87
|
+
const order = nonNegativeInteger(row.sort_order);
|
|
88
|
+
if (!ID_PATTERN.test(viewInstanceId) || !ID_PATTERN.test(key) || !ID_PATTERN.test(dataInstanceId)) invalid("View Component binding 식별자가 올바르지 않습니다.");
|
|
89
|
+
if (!ACCESS_MODES.includes(accessMode) || order === null) invalid(`${viewInstanceId}.${key} binding 계약이 올바르지 않습니다.`);
|
|
90
|
+
return { viewInstanceId, key, dataInstanceId, accessMode, required: row.is_required === true, order };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeProjectViews(viewRows, bindingRows, dataComponents, customRegistry = null) {
|
|
94
|
+
const registry = customRegistry || registryModule.createDefaultRegistry();
|
|
95
|
+
const dataById = new Map((Array.isArray(dataComponents) ? dataComponents : []).map(item => [item.instanceId, item]));
|
|
96
|
+
const views = (Array.isArray(viewRows) ? viewRows : []).map(row => normalizeView(row, registry));
|
|
97
|
+
const byId = new Map();
|
|
98
|
+
const orders = new Set();
|
|
99
|
+
views.forEach(view => {
|
|
100
|
+
if (byId.has(view.viewInstanceId)) invalid(`중복 View Component입니다: ${view.viewInstanceId}`);
|
|
101
|
+
if (orders.has(view.order)) invalid(`중복 View Component 순서입니다: ${view.order}`);
|
|
102
|
+
byId.set(view.viewInstanceId, view);
|
|
103
|
+
orders.add(view.order);
|
|
104
|
+
});
|
|
105
|
+
(Array.isArray(bindingRows) ? bindingRows : []).map(normalizeBinding).forEach(bindingRow => {
|
|
106
|
+
const view = byId.get(bindingRow.viewInstanceId);
|
|
107
|
+
if (!view) invalid(`${bindingRow.viewInstanceId} binding에 대응하는 View Component가 없습니다.`);
|
|
108
|
+
const dataComponent = dataById.get(bindingRow.dataInstanceId);
|
|
109
|
+
if (!dataComponent || dataComponent.enabled !== true || dataComponent.archived === true) {
|
|
110
|
+
invalid(`${view.viewInstanceId}.${bindingRow.key}가 활성 Data Component를 가리키지 않습니다.`);
|
|
111
|
+
}
|
|
112
|
+
const slot = view.definition.bindings.find(item => item.key === bindingRow.key);
|
|
113
|
+
if (!slot) invalid(`${view.viewInstanceId}.${bindingRow.key}는 등록되지 않은 binding입니다.`);
|
|
114
|
+
if (slot.dataTemplateId !== dataComponent.templateId || slot.accessMode !== bindingRow.accessMode || slot.required !== bindingRow.required) {
|
|
115
|
+
invalid(`${view.viewInstanceId}.${bindingRow.key}가 View Template binding 계약과 다릅니다.`);
|
|
116
|
+
}
|
|
117
|
+
view.bindings.push({ ...bindingRow, dataTemplateId: dataComponent.templateId, dataLabel: dataComponent.tab });
|
|
118
|
+
});
|
|
119
|
+
views.forEach(view => {
|
|
120
|
+
view.definition.bindings.filter(item => item.required).forEach(slot => {
|
|
121
|
+
if (!view.bindings.some(item => item.key === slot.key)) invalid(`${view.viewInstanceId}에 필수 binding '${slot.key}'가 없습니다.`);
|
|
122
|
+
});
|
|
123
|
+
view.bindings.sort((left, right) => left.order - right.order);
|
|
124
|
+
delete view.definition;
|
|
125
|
+
});
|
|
126
|
+
const enabledViews = views.filter(view => view.enabled);
|
|
127
|
+
for (const mode of ["authoring", "preview"]) {
|
|
128
|
+
if (!enabledViews.some(view => view.modes.includes(mode))) invalid(`${mode} Mode에 사용할 활성 View Component가 없습니다.`);
|
|
129
|
+
}
|
|
130
|
+
const visibleData = [...dataById.values()].filter(instance => instance.enabled === true
|
|
131
|
+
&& instance.archived !== true
|
|
132
|
+
&& instance.config?.hidden !== true);
|
|
133
|
+
for (const dataComponent of visibleData) {
|
|
134
|
+
const ownerViews = enabledViews.filter(view => view.rendererId === dataComponent.templateId
|
|
135
|
+
&& view.bindings.some(binding => binding.key === "primary"
|
|
136
|
+
&& binding.dataInstanceId === dataComponent.instanceId
|
|
137
|
+
&& binding.accessMode === "read_write"));
|
|
138
|
+
if (!ownerViews.length) invalid(`${dataComponent.instanceId} Data Component를 표시·수정할 활성 View Component가 없습니다.`);
|
|
139
|
+
}
|
|
140
|
+
return views.sort((left, right) => left.order - right.order);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateProjectViews(viewRows, bindingRows, dataComponents, registry = null) {
|
|
144
|
+
try {
|
|
145
|
+
return { ok: true, views: normalizeProjectViews(viewRows, bindingRows, dataComponents, registry), error: null };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return { ok: false, views: [], error };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Object.freeze({ MODES, ACCESS_MODES, normalizeProjectViews, validateProjectViews });
|
|
152
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
let catalogContract = root && root.WriterWorkbenchComponentCatalogContract;
|
|
3
|
+
if (typeof module === "object" && module.exports) catalogContract = require("./component-catalog-contract.js");
|
|
4
|
+
const api = factory(catalogContract);
|
|
5
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
6
|
+
if (root) root.WriterWorkbenchViewComponentRegistry = api;
|
|
7
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (catalogContract) {
|
|
8
|
+
"use strict";
|
|
9
|
+
if (!catalogContract) throw new Error("WriterWorkbenchComponentCatalogContract is required");
|
|
10
|
+
|
|
11
|
+
const VIEW_TEMPLATES = Object.freeze(catalogContract.list()
|
|
12
|
+
.filter(descriptor => descriptor.view)
|
|
13
|
+
.map(descriptor => definition(
|
|
14
|
+
descriptor.view.templateId,
|
|
15
|
+
descriptor.view.label,
|
|
16
|
+
descriptor.view.rendererId,
|
|
17
|
+
descriptor.view.bindings.map(item => binding(item.key, item.dataTemplateId, item.accessMode, item.required)),
|
|
18
|
+
descriptor.view.supportedModes
|
|
19
|
+
)));
|
|
20
|
+
|
|
21
|
+
function binding(key, dataTemplateId, accessMode, required = true) {
|
|
22
|
+
return Object.freeze({ key, dataTemplateId, accessMode, required });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function definition(templateId, label, rendererId, bindings, supportedModes = ["authoring", "preview"]) {
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
templateId,
|
|
28
|
+
label,
|
|
29
|
+
rendererId,
|
|
30
|
+
supportedModes: Object.freeze([...supportedModes]),
|
|
31
|
+
bindings: Object.freeze(bindings)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createDefaultRegistry() {
|
|
36
|
+
const definitions = new Map(VIEW_TEMPLATES.map(item => [item.templateId, item]));
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
get: templateId => definitions.get(String(templateId || "")) || null,
|
|
39
|
+
list: () => [...definitions.values()]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.freeze({ VIEW_TEMPLATES, createDefaultRegistry });
|
|
44
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.WriterWorkbenchViewComponentRuntime = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const WORKBENCH_MODES = Object.freeze(["authoring", "preview"]);
|
|
9
|
+
|
|
10
|
+
function invalid(message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.code = "SOMI_VIEW_RUNTIME_INVALID";
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function immutableClone(value) {
|
|
17
|
+
const clone = value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
18
|
+
function freezeDeep(next) {
|
|
19
|
+
if (!next || typeof next !== "object" || Object.isFrozen(next)) return next;
|
|
20
|
+
Object.values(next).forEach(freezeDeep);
|
|
21
|
+
return Object.freeze(next);
|
|
22
|
+
}
|
|
23
|
+
return freezeDeep(clone);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function create(viewComponents, dataComponents) {
|
|
27
|
+
if (!Array.isArray(viewComponents) || !Array.isArray(dataComponents)) {
|
|
28
|
+
invalid("View 런타임 입력이 올바르지 않습니다.");
|
|
29
|
+
}
|
|
30
|
+
const dataById = new Map(dataComponents.map(instance => [instance.instanceId, instance]));
|
|
31
|
+
const views = viewComponents.filter(view => view?.enabled === true).sort((left, right) => left.order - right.order);
|
|
32
|
+
const viewById = new Map(views.map(view => [view.viewInstanceId, view]));
|
|
33
|
+
|
|
34
|
+
function bindings(viewOrId, dataResolver = instance => instance.data) {
|
|
35
|
+
const view = typeof viewOrId === "string" ? viewById.get(viewOrId) : viewOrId;
|
|
36
|
+
if (!view || !viewById.has(view.viewInstanceId)) invalid("활성 View Component를 찾을 수 없습니다.");
|
|
37
|
+
const records = {};
|
|
38
|
+
for (const binding of view.bindings || []) {
|
|
39
|
+
const instance = dataById.get(binding.dataInstanceId);
|
|
40
|
+
if (!instance) invalid(`${view.viewInstanceId}.${binding.key} Data Component를 찾을 수 없습니다.`);
|
|
41
|
+
const writable = binding.accessMode === "read_write";
|
|
42
|
+
const resolvedData = dataResolver(instance, binding);
|
|
43
|
+
records[binding.key] = Object.freeze({
|
|
44
|
+
binding,
|
|
45
|
+
instance,
|
|
46
|
+
data: writable ? resolvedData : immutableClone(resolvedData),
|
|
47
|
+
accessMode: binding.accessMode,
|
|
48
|
+
writable
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const primary = records.primary;
|
|
52
|
+
if (!primary) invalid(`${view.viewInstanceId}에 primary binding이 없습니다.`);
|
|
53
|
+
if (view.rendererId !== primary.instance.templateId) {
|
|
54
|
+
invalid(`${view.viewInstanceId} renderer와 primary Data Component가 일치하지 않습니다.`);
|
|
55
|
+
}
|
|
56
|
+
return Object.freeze({ view, primary, bindings: Object.freeze(records) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function list(mode) {
|
|
60
|
+
if (!WORKBENCH_MODES.includes(mode)) return [];
|
|
61
|
+
return views.filter(view => Array.isArray(view.modes) && view.modes.includes(mode));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function find(mode, requestedId) {
|
|
65
|
+
const candidates = list(mode);
|
|
66
|
+
const requested = String(requestedId || "");
|
|
67
|
+
if (!requested) return candidates[0] || null;
|
|
68
|
+
return candidates.find(view => view.viewInstanceId === requested)
|
|
69
|
+
|| candidates.find(view => bindings(view).primary.instance.instanceId === requested)
|
|
70
|
+
|| null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function primary(viewOrId) {
|
|
74
|
+
return bindings(viewOrId).primary.instance;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tabLabel(viewOrId) {
|
|
78
|
+
return String(primary(viewOrId).tab || bindings(viewOrId).view.label || "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writableInstanceIds() {
|
|
82
|
+
const ids = new Set();
|
|
83
|
+
for (const view of views) {
|
|
84
|
+
for (const record of Object.values(bindings(view).bindings)) {
|
|
85
|
+
if (record.writable && record.instance.templateId !== "ssobig.assets") ids.add(record.instance.instanceId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...ids];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Object.freeze({ views: Object.freeze(views), list, find, bindings, primary, tabLabel, writableInstanceIds });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({ WORKBENCH_MODES, create });
|
|
95
|
+
});
|