@operato/app 10.8.1 → 10.10.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/CHANGELOG.md +44 -0
- package/dist/src/grist-editor/ox-grist-editor-code.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-code.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-code.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-json.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-json.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-json.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-privilege.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-privilege.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-privilege.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-resource-code.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-code.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-code.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-resource-id.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-id.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-id.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-resource-object-legacy.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-object-legacy.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-object-legacy.js.map +1 -1
- package/dist/src/grist-editor/ox-grist-editor-resource-object.d.ts +1 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-object.js +2 -0
- package/dist/src/grist-editor/ox-grist-editor-resource-object.js.map +1 -1
- package/dist/src/relation/ox-relation-view.d.ts +127 -0
- package/dist/src/relation/ox-relation-view.js +319 -0
- package/dist/src/relation/ox-relation-view.js.map +1 -0
- package/dist/src/relation/relation-groups.d.ts +33 -0
- package/dist/src/relation/relation-groups.js +142 -0
- package/dist/src/relation/relation-groups.js.map +1 -0
- package/dist/src/selector/ox-selector-resource-object-legacy.js +13 -1
- package/dist/src/selector/ox-selector-resource-object-legacy.js.map +1 -1
- package/dist/src/selector/ox-selector-resource-object.d.ts +40 -2
- package/dist/src/selector/ox-selector-resource-object.js +180 -16
- package/dist/src/selector/ox-selector-resource-object.js.map +1 -1
- package/dist/test/selector-confirm-keeps-value.test.d.ts +1 -0
- package/dist/test/selector-confirm-keeps-value.test.js +108 -0
- package/dist/test/selector-confirm-keeps-value.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +61 -53
- package/test/selector-confirm-keeps-value.test.ts +127 -0
- package/translations/en.json +24 -0
- package/translations/es.json +26 -0
- package/translations/ja.json +26 -0
- package/translations/ko.json +24 -0
- package/translations/ms.json +6 -0
- package/translations/zh.json +24 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
var OxRelationView_1;
|
|
2
|
+
import { __decorate } from "tslib";
|
|
3
|
+
import { LitElement, css, html, svg } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
let OxRelationView = OxRelationView_1 = class OxRelationView extends LitElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(...arguments);
|
|
8
|
+
/** 가운데 — 지금 보고 있는 것. */
|
|
9
|
+
this.center = { id: '' };
|
|
10
|
+
this.groups = [];
|
|
11
|
+
/** 이웃이 없는 관계도 그루터기로 남길까. 기본은 **남긴다**(부재가 정보다). */
|
|
12
|
+
this.hideEmpty = false;
|
|
13
|
+
/** 선을 그리려면 위치를 재야 한다 — 재고 나서 한 번 더 그린다. */
|
|
14
|
+
this.edges = [];
|
|
15
|
+
this.byTense = (a, b) => { var _a, _b, _c, _d; return ((_b = OxRelationView_1.TENSE_ORDER[(_a = a.tense) !== null && _a !== void 0 ? _a : '']) !== null && _b !== void 0 ? _b : 2) - ((_d = OxRelationView_1.TENSE_ORDER[(_c = b.tense) !== null && _c !== void 0 ? _c : '']) !== null && _d !== void 0 ? _d : 2); };
|
|
16
|
+
}
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
super.connectedCallback();
|
|
19
|
+
/* 폭이 바뀌면 선의 좌표가 통째로 달라진다 — 다시 재지 않으면 선이 노드에서 떨어져 나간다. */
|
|
20
|
+
this.ro = new ResizeObserver(() => this.measure());
|
|
21
|
+
this.ro.observe(this);
|
|
22
|
+
}
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
var _a;
|
|
25
|
+
(_a = this.ro) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
26
|
+
super.disconnectedCallback();
|
|
27
|
+
}
|
|
28
|
+
updated() {
|
|
29
|
+
this.measure();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 선을 잇는다 — **그린 뒤에 재고, 잰 값이 달라졌을 때만 다시 그린다.**
|
|
33
|
+
* 조건 없이 상태를 쓰면 `updated` → `measure` → `updated` 로 무한히 반복된다.
|
|
34
|
+
*/
|
|
35
|
+
measure() {
|
|
36
|
+
const root = this.renderRoot;
|
|
37
|
+
const wrap = root.querySelector('.wrap');
|
|
38
|
+
const centerEl = root.querySelector('.center');
|
|
39
|
+
if (!wrap || !centerEl)
|
|
40
|
+
return;
|
|
41
|
+
/* 세로로 접힌 좁은 화면에서는 선이 뜻을 잃는다 — 그럴 때는 아예 그리지 않는다. */
|
|
42
|
+
const stacked = getComputedStyle(wrap).gridTemplateColumns.split(' ').length < 3;
|
|
43
|
+
const base = wrap.getBoundingClientRect();
|
|
44
|
+
const c = centerEl.getBoundingClientRect();
|
|
45
|
+
const next = stacked
|
|
46
|
+
? []
|
|
47
|
+
: /*
|
|
48
|
+
* **노드 줄의 중심에서 자기 부모의 중심으로** 잇는다.
|
|
49
|
+
*
|
|
50
|
+
* 그룹 전체(`.grp`)가 아니라 노드 줄(`.row`)을 측정한다. 그룹을 측정하면 위에 얹힌 관계 이름까지
|
|
51
|
+
* 중심에 들어가 선이 상자보다 위로 치우친다.
|
|
52
|
+
*/
|
|
53
|
+
/*
|
|
54
|
+
* Array.from 을 쓴다 — 이 꾸러미의 tsconfig 는 NodeList 펼치기를 받지 않는다
|
|
55
|
+
* (downlevelIteration 없음). 트윈에서 옮겨 오면서 여기서만 걸렸다.
|
|
56
|
+
*/
|
|
57
|
+
Array.from(root.querySelectorAll('.grp .row')).map(row => {
|
|
58
|
+
var _a;
|
|
59
|
+
const g = row.parentElement;
|
|
60
|
+
const a = c;
|
|
61
|
+
const r = row.getBoundingClientRect();
|
|
62
|
+
const isIn = !!g.closest('.col.in');
|
|
63
|
+
const gy = r.top - base.top + r.height / 2;
|
|
64
|
+
const gx = (isIn ? r.right : r.left) - base.left;
|
|
65
|
+
const ax = (isIn ? a.left : a.right) - base.left;
|
|
66
|
+
const ay = a.top - base.top + a.height / 2;
|
|
67
|
+
return {
|
|
68
|
+
x1: isIn ? gx : ax,
|
|
69
|
+
y1: isIn ? gy : ay,
|
|
70
|
+
x2: isIn ? ax : gx,
|
|
71
|
+
y2: isIn ? ay : gy,
|
|
72
|
+
label: '',
|
|
73
|
+
state: (_a = g.dataset.state) !== null && _a !== void 0 ? _a : 'linked'
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const same = next.length === this.edges.length && next.every((e, i) => {
|
|
77
|
+
const p = this.edges[i];
|
|
78
|
+
return Math.abs(p.x1 - e.x1) < 0.5 && Math.abs(p.y1 - e.y1) < 0.5 && Math.abs(p.x2 - e.x2) < 0.5 && Math.abs(p.y2 - e.y2) < 0.5 && p.state === e.state;
|
|
79
|
+
});
|
|
80
|
+
if (!same)
|
|
81
|
+
this.edges = next;
|
|
82
|
+
}
|
|
83
|
+
pick(n) {
|
|
84
|
+
if (n.reachable === false)
|
|
85
|
+
return;
|
|
86
|
+
this.dispatchEvent(new CustomEvent('pick', { detail: n, bubbles: true, composed: true }));
|
|
87
|
+
}
|
|
88
|
+
expand(g) {
|
|
89
|
+
this.dispatchEvent(new CustomEvent('expand', { detail: g, bubbles: true, composed: true }));
|
|
90
|
+
}
|
|
91
|
+
render() {
|
|
92
|
+
var _a, _b;
|
|
93
|
+
/*
|
|
94
|
+
* 「비었다」를 감출 때도 **못 푼 것은 남긴다.** 그것은 없다는 뜻이 아니라 답을 못 얻었다는
|
|
95
|
+
* 뜻이라, 감추면 화면이 「없다」고 말한 것이 된다.
|
|
96
|
+
*/
|
|
97
|
+
const shown = this.hideEmpty ? this.groups.filter(g => g.nodes.length || g.unresolved) : this.groups;
|
|
98
|
+
const incoming = shown.filter(g => g.direction === 'in').sort(this.byTense);
|
|
99
|
+
const outgoing = shown.filter(g => g.direction === 'out').sort(this.byTense);
|
|
100
|
+
return html `
|
|
101
|
+
<div class="wrap">
|
|
102
|
+
<svg class="wires" aria-hidden="true">
|
|
103
|
+
${this.edges.map(e => svg `
|
|
104
|
+
<path d="M${e.x1} ${e.y1} C ${(e.x1 + e.x2) / 2} ${e.y1}, ${(e.x1 + e.x2) / 2} ${e.y2}, ${e.x2} ${e.y2}"
|
|
105
|
+
fill="none" stroke-width="1.2"
|
|
106
|
+
stroke=${e.state === 'broken' ? 'var(--ox-rv-warn)' : e.state === 'stub' ? 'var(--ox-rv-line)' : 'var(--ox-rv-accent)'}
|
|
107
|
+
stroke-dasharray=${e.state === 'stub' ? '3 3' : e.state === 'observed' ? '1 3' : ''}
|
|
108
|
+
opacity=${e.state === 'stub' ? '0.6' : '0.85'} />`)}
|
|
109
|
+
</svg>
|
|
110
|
+
|
|
111
|
+
<div class="col in">
|
|
112
|
+
${incoming.length ? incoming.map(g => this.renderGroup(g)) : html `<span class="none"></span>`}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="center">
|
|
116
|
+
${this.center.label && this.center.label !== this.center.id
|
|
117
|
+
? html `${this.center.label}<span class="cid">${(_a = this.center.sub) !== null && _a !== void 0 ? _a : this.center.id}</span>`
|
|
118
|
+
: (_b = this.center.sub) !== null && _b !== void 0 ? _b : this.center.id}
|
|
119
|
+
${this.center.kind ? html `<span class="kind">${this.center.kind}</span>` : ''}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="col out">
|
|
123
|
+
${outgoing.length ? outgoing.map(g => this.renderGroup(g)) : html `<span class="none"></span>`}
|
|
124
|
+
</div>
|
|
125
|
+
</div>`;
|
|
126
|
+
}
|
|
127
|
+
/** 한 묶음 — 관계 이름과 그 관계로 이어진 노드들. */
|
|
128
|
+
renderGroup(g) {
|
|
129
|
+
var _a, _b, _c, _d;
|
|
130
|
+
const total = (_a = g.total) !== null && _a !== void 0 ? _a : g.nodes.length;
|
|
131
|
+
const hidden = Math.max(0, total - g.nodes.length);
|
|
132
|
+
const state = g.nodes.some(n => n.broken)
|
|
133
|
+
? 'broken'
|
|
134
|
+
: !g.nodes.length
|
|
135
|
+
? /*
|
|
136
|
+
* 수가 있으면 이어져 있는 것이다 — 식별자가 안 왔을 뿐이다. 그루터기(점선)로 그리면
|
|
137
|
+
* 선까지 「없음」이라고 말한다.
|
|
138
|
+
*/
|
|
139
|
+
total > 0
|
|
140
|
+
? 'linked'
|
|
141
|
+
: /* 선언이 「언제나 있다」고 한 자리가 비었으면 그것은 없음이 아니라 **끊어진 참조**다. */
|
|
142
|
+
g.optional === false
|
|
143
|
+
? 'broken'
|
|
144
|
+
: 'stub'
|
|
145
|
+
: g.source === 'observed'
|
|
146
|
+
? 'observed'
|
|
147
|
+
: 'linked';
|
|
148
|
+
return html `
|
|
149
|
+
<div class="grp" data-state=${state}>
|
|
150
|
+
<!-- 선언인지 관측인지 밝힌다 — 섞이면 "모델이 그렇게 생겼다" 와 "실제로 그렇게 움직였다"
|
|
151
|
+
를 구별할 수 없다. 관측분은 근거 범위도 함께 말한다(오류보고없이 자르지 않는다). -->
|
|
152
|
+
<span class="via ${g.source === 'observed' ? 'obs' : ''}" title=${(_b = g.basis) !== null && _b !== void 0 ? _b : ''}
|
|
153
|
+
>${g.via}${g.basis ? html ` <span class="basis">${g.basis}</span>` : ''}${g.tenseLabel
|
|
154
|
+
? html ` <span class="tense" data-t=${(_c = g.tense) !== null && _c !== void 0 ? _c : ''}>${g.tenseLabel}</span>`
|
|
155
|
+
: ''}</span
|
|
156
|
+
>
|
|
157
|
+
<div class="row">
|
|
158
|
+
${g.nodes.length
|
|
159
|
+
? html `
|
|
160
|
+
${g.nodes.map(n => this.renderNode(n))}
|
|
161
|
+
${hidden ? html `<button class="more" @click=${() => this.expand(g)}>+${hidden}</button>` : ''}`
|
|
162
|
+
: total > 0
|
|
163
|
+
? /*
|
|
164
|
+
* ── 수는 있는데 **하나하나가 안 온 것** ──────────────────────
|
|
165
|
+
* 보내는 쪽이 일부러 식별자를 안 보내는 관계가 있다(설비 상태 구간처럼 사람이 하나를
|
|
166
|
+
* 골라 갈 대상이 아닌 것). `ids` 만 보고 그리면 6건이 0건으로 보인다 — 화면이
|
|
167
|
+
* 없는 사실을 말하게 된다. **수를 믿고 목록으로 넘긴다.**
|
|
168
|
+
*/
|
|
169
|
+
html `<button class="more" @click=${() => this.expand(g)}>${total}</button>`
|
|
170
|
+
: g.unresolved
|
|
171
|
+
? /*
|
|
172
|
+
* 못 풀었다. **「없다」로 그리지 않는다** — 사람이 할 일이 다르다(앞은 자료를 채울
|
|
173
|
+
* 일, 뒤는 푸는 쪽을 꽂을 일).
|
|
174
|
+
*/
|
|
175
|
+
html `<span class="node unknown" title=${(_d = g.unresolvedNote) !== null && _d !== void 0 ? _d : g.unresolved}>?</span>`
|
|
176
|
+
: /* 이웃이 없다 — 선언은 있다. 선택 관계인지에 따라 다르게 읽힌다. */
|
|
177
|
+
html `<span class="node stub ${g.optional ? '' : 'req'}">${g.optional ? '—' : '∅'}</span>`}
|
|
178
|
+
</div>
|
|
179
|
+
</div>`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 노드 하나.
|
|
183
|
+
*
|
|
184
|
+
* **제자리에서 더 펼치지 않는다.** 한때 노드마다 자기 이웃을 오른쪽에 이어 그리게 했는데,
|
|
185
|
+
* 양방향 그래프에서는 자식이 거의 언제나 부모를 도로 보여 준다 — 같은 id 가 사방에 되풀이되고
|
|
186
|
+
* 그림이 아니라 털뭉치가 된다. 깊이는 **걸어서** 간다(노드를 누르면 그것이 가운데가 된다).
|
|
187
|
+
* 한 방향으로 여러 단을 봐야 하는 것(BOM 전개)은 되돌아오는 선이 없는 **다른 읽기**다.
|
|
188
|
+
*/
|
|
189
|
+
renderNode(n) {
|
|
190
|
+
var _a, _b;
|
|
191
|
+
/*
|
|
192
|
+
* **이름이 중심이다.** id 만 그리면 그림이 `cutter-op` · `cut-station` 으로 채워져, 관계는
|
|
193
|
+
* 보이는데 무엇과 무엇의 관계인지는 안 보인다. 이름이 있으면 이름을 앞에 세운다.
|
|
194
|
+
*
|
|
195
|
+
* 그래도 id 를 지우지 않는다 — 걷는 손잡이이고, 같은 이름을 가진 것이 둘일 때 구별하는 유일한
|
|
196
|
+
* 값이다. 작게 아래에 둔다(이름이 없으면 id 하나만 남는다).
|
|
197
|
+
*/
|
|
198
|
+
const named = !!n.label && n.label !== n.id;
|
|
199
|
+
return html `
|
|
200
|
+
<button class="node ${n.reachable === false ? 'flat' : ''} ${n.broken ? 'broken' : ''}"
|
|
201
|
+
?disabled=${n.reachable === false} title=${n.id} @click=${() => this.pick(n)}
|
|
202
|
+
>${named ? html `${n.label}<span class="nid">${(_a = n.sub) !== null && _a !== void 0 ? _a : n.id}</span>` : ((_b = n.sub) !== null && _b !== void 0 ? _b : n.id)}</button>`;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
OxRelationView.styles = css `
|
|
206
|
+
:host { display: block; position: relative; --ox-rv-line: var(--line, #d5dce0); --ox-rv-ink: var(--ink, #17242c);
|
|
207
|
+
--ox-rv-muted: var(--muted, #57676f); --ox-rv-faint: var(--faint, #8a959c);
|
|
208
|
+
--ox-rv-accent: var(--accent, #0d8f86); --ox-rv-warn: var(--warn, #c60); --ox-rv-panel: var(--panel, #fff); }
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* 라벨 자리는 **바깥 여백**으로 낸다.
|
|
212
|
+
*
|
|
213
|
+
* 열에 위쪽 여백을 주면 열 상자만 아래로 밀린다 — 가운데 정렬은 상자를 맞추므로
|
|
214
|
+
* 내용은 그 절반만큼 내려가고, 묶음이 하나뿐일 때도 선이 그만큼 휜다. 여백을 컨테이너에 주면
|
|
215
|
+
* 가운데 노드와 열이 **같이** 밀려 상대 정렬이 그대로 남는다.
|
|
216
|
+
*/
|
|
217
|
+
.wrap { position: relative; display: grid; grid-template-columns: 1fr auto 1fr; gap: 26px; align-items: center; padding-top: 15px; }
|
|
218
|
+
/* 좁으면 세로로 배치한다. 그렇게 되면 선이 뜻을 잃으므로 그때는 선을 그리지 않는다. */
|
|
219
|
+
@media (max-width: 720px) { .wrap { grid-template-columns: 1fr; gap: 10px; } }
|
|
220
|
+
|
|
221
|
+
svg.wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
222
|
+
svg.wires text { font-family: var(--mono, monospace); font-size: 9.5px; fill: var(--ox-rv-faint); }
|
|
223
|
+
|
|
224
|
+
.col { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
|
|
225
|
+
.col.in { align-items: flex-end; text-align: right; }
|
|
226
|
+
.col.out { align-items: flex-start; }
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* 묶음의 높이 = **노드 줄의 높이.** 관계 이름은 아이템 위에 얹힌 장식이지 레이아웃 참여자가
|
|
230
|
+
* 아니다. 일반 배치에 두면 그룹의 중심이 라벨만큼 내려가고, 그만큼 선이 휘어 노드에 비스듬히 닿는다
|
|
231
|
+
* — 선을 잘 휘게 만들 일이 아니라 애초에 같은 높이에 두어야 하는 일이다.
|
|
232
|
+
*/
|
|
233
|
+
.grp { position: relative; display: flex; max-width: 100%; }
|
|
234
|
+
.grp .via {
|
|
235
|
+
position: absolute; bottom: calc(100% + 2px); white-space: nowrap;
|
|
236
|
+
font-family: var(--mono, monospace); font-size: 9.5px; color: var(--ox-rv-faint); letter-spacing: .03em;
|
|
237
|
+
}
|
|
238
|
+
.col.in .grp .via { right: 0; }
|
|
239
|
+
.col.out .grp .via { left: 0; }
|
|
240
|
+
/* 관측에서 온 이어짐 — 선언과 같은 모양으로 두면 모델이 그렇게 생긴 줄로 읽힌다. */
|
|
241
|
+
.grp .via.obs::before { content: '◦ '; color: var(--ox-rv-accent); }
|
|
242
|
+
.grp .via .basis { opacity: .75; font-style: italic; }
|
|
243
|
+
.grp .row { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
244
|
+
.col.in .grp .row { justify-content: flex-end; }
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* 칩 **안의 글은 왼쪽으로** (2026-08-25 · 사용자 지시). 칩 상자의 자리는 묶음이 정하고(들어오는
|
|
248
|
+
* 쪽은 오른쪽에 붙는다) 그것은 그대로 둔다 — 상자 안에서 이름과 코드가 같은 자리에서 시작해야
|
|
249
|
+
* 여러 줄을 눈으로 훑을 수 있다.
|
|
250
|
+
*/
|
|
251
|
+
.node {
|
|
252
|
+
border: 1px solid var(--ox-rv-line); border-radius: 7px; background: var(--ox-rv-panel);
|
|
253
|
+
padding: 3px 9px; font-size: 11.5px; color: var(--ox-rv-accent); cursor: pointer; max-width: 220px;
|
|
254
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;
|
|
255
|
+
}
|
|
256
|
+
.node:hover { border-color: var(--ox-rv-accent); }
|
|
257
|
+
/* 갈 곳이 없는 값 — 문처럼 보이지 않는다. */
|
|
258
|
+
.node.flat { color: var(--ox-rv-muted); border-style: dashed; cursor: default; }
|
|
259
|
+
.node.broken { color: var(--ox-rv-warn); border-color: color-mix(in oklab, var(--ox-rv-warn), transparent 45%); }
|
|
260
|
+
/* 선언은 있는데 이웃이 없다 — 그루터기. */
|
|
261
|
+
.node.stub { color: var(--ox-rv-faint); border-style: dotted; cursor: default; font-style: italic; }
|
|
262
|
+
.node.stub.req { color: var(--ox-rv-warn); border-color: color-mix(in oklab, var(--ox-rv-warn), transparent 55%); }
|
|
263
|
+
|
|
264
|
+
/* 못 푼 것 — 비어 있는 것과 **다르게** 보여야 한다. 물음표는 「모른다」이지 「없다」가 아니다. */
|
|
265
|
+
.node.unknown { color: var(--ox-rv-muted); border-style: dashed; border-color: var(--ox-rv-muted); cursor: help; font-style: normal; }
|
|
266
|
+
|
|
267
|
+
/* 때 표식 — 관계 이름 옆에 붙는 작은 말. 지난 일은 흐리게, 앞의 일은 눈에 걸리게. */
|
|
268
|
+
.grp .via .tense { font-style: normal; letter-spacing: 0; padding: 0 3px; border-radius: 3px; }
|
|
269
|
+
.grp .via .tense[data-t='past'] { color: var(--ox-rv-faint); }
|
|
270
|
+
.grp .via .tense[data-t='future'] { color: var(--ox-rv-warn); }
|
|
271
|
+
.grp .via .tense[data-t='current'] { color: var(--ox-rv-accent); }
|
|
272
|
+
.grp .via .tense[data-t='standing'] { color: var(--ox-rv-muted); }
|
|
273
|
+
|
|
274
|
+
.more { border: 0; background: transparent; color: var(--ox-rv-accent); font-size: 11px; cursor: pointer; padding: 3px 4px; }
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
.center {
|
|
278
|
+
border: 1.6px solid var(--ox-rv-accent); border-radius: 9px; padding: 7px 13px;
|
|
279
|
+
background: color-mix(in oklab, var(--ox-rv-accent), transparent 92%);
|
|
280
|
+
font-weight: 700; font-size: 13px; color: var(--ox-rv-ink); text-align: center; white-space: nowrap;
|
|
281
|
+
}
|
|
282
|
+
.center .kind { display: block; font-family: var(--mono, monospace); font-weight: 400; font-size: 9.5px; color: var(--ox-rv-faint); }
|
|
283
|
+
/* id 는 이름 아래 작게 — 손잡이이자 동명이인을 구분하는 값이라 지우지 않는다. */
|
|
284
|
+
.center .cid { display: block; font-family: var(--mono, monospace); font-weight: 400; font-size: 10px; color: var(--ox-rv-muted); }
|
|
285
|
+
/*
|
|
286
|
+
* 식별자 줄은 **왼쪽으로 붙인다** (2026-08-25 · 사용자 지시).
|
|
287
|
+
*
|
|
288
|
+
* 이름은 길이가 저마다여서 가운데로 두면 아래의 코드가 줄마다 다른 자리에서 시작한다 — 코드는
|
|
289
|
+
* 눈으로 훑어 대조하는 값이라 시작 자리가 같아야 읽힌다. 이름은 그대로 두고 코드만 붙인다.
|
|
290
|
+
*/
|
|
291
|
+
.node .nid { display: block; text-align: left; font-family: var(--mono, monospace); font-size: 9.5px; color: var(--ox-rv-faint); }
|
|
292
|
+
.center .cid { text-align: left; }
|
|
293
|
+
.none { font-size: 11.5px; color: var(--ox-rv-faint); }
|
|
294
|
+
`;
|
|
295
|
+
/**
|
|
296
|
+
* 때 순서 — **아직 걸릴 수 있는 것이 위**다.
|
|
297
|
+
*
|
|
298
|
+
* 들어오는 묶음이 여럿일 때 눈이 먼저 닿는 자리에 「앞으로 걸리는 것」을 둔다. 지난 일은 무엇을
|
|
299
|
+
* 해도 안 바뀌므로 아래다. 때를 말하지 않은 것(선언이 줄마다 다르다고 남겨 둔 것)은 가운데에
|
|
300
|
+
* 둔다 — 「없다」가 아니라 「봐야 안다」이므로 지난 일보다 앞이다.
|
|
301
|
+
*/
|
|
302
|
+
OxRelationView.TENSE_ORDER = { future: 0, current: 1, standing: 3, past: 4 };
|
|
303
|
+
__decorate([
|
|
304
|
+
property({ attribute: false })
|
|
305
|
+
], OxRelationView.prototype, "center", void 0);
|
|
306
|
+
__decorate([
|
|
307
|
+
property({ attribute: false })
|
|
308
|
+
], OxRelationView.prototype, "groups", void 0);
|
|
309
|
+
__decorate([
|
|
310
|
+
property({ type: Boolean })
|
|
311
|
+
], OxRelationView.prototype, "hideEmpty", void 0);
|
|
312
|
+
__decorate([
|
|
313
|
+
state()
|
|
314
|
+
], OxRelationView.prototype, "edges", void 0);
|
|
315
|
+
OxRelationView = OxRelationView_1 = __decorate([
|
|
316
|
+
customElement('ox-relation-view')
|
|
317
|
+
], OxRelationView);
|
|
318
|
+
export { OxRelationView };
|
|
319
|
+
//# sourceMappingURL=ox-relation-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ox-relation-view.js","sourceRoot":"","sources":["../../../src/relation/ox-relation-view.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAuB,MAAM,KAAK,CAAA;AACrE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAiH3D,IAAM,cAAc,sBAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QACL,wBAAwB;QACQ,WAAM,GAAiB,EAAE,EAAE,EAAE,EAAE,EAAE,CAAA;QACjC,WAAM,GAAoB,EAAE,CAAA;QAC5D,kDAAkD;QACrB,cAAS,GAAG,KAAK,CAAA;QAE9C,2CAA2C;QAC1B,UAAK,GAAuF,EAAE,CAAA;QAsLvG,YAAO,GAAG,CAAC,CAAgB,EAAE,CAAgB,EAAE,EAAE,uBACvD,OAAA,CAAC,MAAA,gBAAc,CAAC,WAAW,CAAC,MAAA,CAAC,CAAC,KAAK,mCAAI,EAAE,CAAC,mCAAI,CAAC,CAAC,GAAG,CAAC,MAAA,gBAAc,CAAC,WAAW,CAAC,MAAA,CAAC,CAAC,KAAK,mCAAI,EAAE,CAAC,mCAAI,CAAC,CAAC,CAAA,EAAA,CAAA;IAmHvG,CAAC;IA3MC,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,yDAAyD;QACzD,IAAI,CAAC,EAAE,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QAClD,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;IAED,oBAAoB;;QAClB,MAAA,IAAI,CAAC,EAAE,0CAAE,UAAU,EAAE,CAAA;QACrB,KAAK,CAAC,oBAAoB,EAAE,CAAA;IAC9B,CAAC;IAES,OAAO;QACf,IAAI,CAAC,OAAO,EAAE,CAAA;IAChB,CAAC;IAED;;;OAGG;IACK,OAAO;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,UAAwB,CAAA;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAuB,CAAA;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAuB,CAAA;QACpE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QAE9B,mDAAmD;QACnD,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;QAChF,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;QACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,qBAAqB,EAAE,CAAA;QAE1C,MAAM,IAAI,GAAG,OAAO;YAClB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;;;;;iBAKG;gBACH;;;mBAGG;gBACH,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAc,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;;oBACpE,MAAM,CAAC,GAAG,GAAG,CAAC,aAA4B,CAAA;oBAC1C,MAAM,CAAC,GAAG,CAAC,CAAA;oBACX,MAAM,CAAC,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAA;oBACrC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;oBACnC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;oBAC1C,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAA;oBAChD,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,CAAA;oBAChD,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;oBAC1C,OAAO;wBACL,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;wBAClB,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;wBAClB,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;wBAClB,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;wBAClB,KAAK,EAAE,EAAE;wBACT,KAAK,EAAE,MAAA,CAAC,CAAC,OAAO,CAAC,KAAK,mCAAI,QAAQ;qBACnC,CAAA;gBACH,CAAC,CAAC,CAAA;QAEN,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACpE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACvB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAA;QACxJ,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;IAC9B,CAAC;IAEO,IAAI,CAAC,CAAe;QAC1B,IAAI,CAAC,CAAC,SAAS,KAAK,KAAK;YAAE,OAAM;QACjC,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC3F,CAAC;IAEO,MAAM,CAAC,CAAgB;QAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC7F,CAAC;IAcD,MAAM;;QACJ;;;WAGG;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAA;QACpG,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAE5E,OAAO,IAAI,CAAA;;;YAGH,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAA;wBACX,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE;;uBAE3F,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB;iCACnG,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;wBACzE,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC;;;;YAIrD,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,4BAA4B;;;;YAI3F,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE;YACzD,CAAC,CAAC,IAAI,CAAA,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,qBAAqB,MAAA,IAAI,CAAC,MAAM,CAAC,GAAG,mCAAI,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS;YACzF,CAAC,CAAC,MAAA,IAAI,CAAC,MAAM,CAAC,GAAG,mCAAI,IAAI,CAAC,MAAM,CAAC,EAAE;YACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,sBAAsB,IAAI,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE;;;;YAI3E,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,4BAA4B;;aAE1F,CAAA;IACX,CAAC;IAED,mCAAmC;IAC3B,WAAW,CAAC,CAAgB;;QAClC,MAAM,KAAK,GAAG,MAAA,CAAC,CAAC,KAAK,mCAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAClD,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;YACvC,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM;gBACf,CAAC,CAAC;;;qBAGG;oBACH,KAAK,GAAG,CAAC;wBACT,CAAC,CAAC,QAAQ;wBACV,CAAC,CAAC,uDAAuD;4BACvD,CAAC,CAAC,QAAQ,KAAK,KAAK;gCACpB,CAAC,CAAC,QAAQ;gCACV,CAAC,CAAC,MAAM;gBACZ,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU;oBACvB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,QAAQ,CAAA;QAChB,OAAO,IAAI,CAAA;oCACqB,KAAK;;;2BAGd,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,MAAA,CAAC,CAAC,KAAK,mCAAI,EAAE;aAC1E,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,wBAAwB,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,UAAU;YACnF,CAAC,CAAC,IAAI,CAAA,+BAA+B,MAAA,CAAC,CAAC,KAAK,mCAAI,EAAE,IAAI,CAAC,CAAC,UAAU,SAAS;YAC3E,CAAC,CAAC,EAAE;;;YAGJ,CAAC,CAAC,KAAK,CAAC,MAAM;YACd,CAAC,CAAC,IAAI,CAAA;kBACA,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;kBACpC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,+BAA+B,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE;YACnG,CAAC,CAAC,KAAK,GAAG,CAAC;gBACT,CAAC,CAAC;;;;;qBAKG;oBACH,IAAI,CAAA,+BAA+B,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW;gBAC7E,CAAC,CAAC,CAAC,CAAC,UAAU;oBACd,CAAC,CAAC;;;yBAGG;wBACH,IAAI,CAAA,oCAAoC,MAAA,CAAC,CAAC,cAAc,mCAAI,CAAC,CAAC,UAAU,WAAW;oBACrF,CAAC,CAAC,2CAA2C;wBAC3C,IAAI,CAAA,0BAA0B,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS;;aAE5F,CAAA;IACX,CAAC;IAED;;;;;;;OAOG;IACK,UAAU,CAAC,CAAe;;QAChC;;;;;;WAMG;QACH,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE,CAAA;QAC3C,OAAO,IAAI,CAAA;4BACa,CAAC,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;oBACvE,CAAC,CAAC,SAAS,KAAK,KAAK,UAAU,CAAC,CAAC,EAAE,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;WACzE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,GAAG,CAAC,CAAC,KAAK,qBAAqB,MAAA,CAAC,CAAC,GAAG,mCAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,MAAA,CAAC,CAAC,GAAG,mCAAI,CAAC,CAAC,EAAE,CAAC,WAAW,CAAA;IACvG,CAAC;;AArSM,qBAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFlB,AAzFY,CAyFZ;AAgFD;;;;;;GAMG;AACqB,0BAAW,GAA2B,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,AAA1E,CAA0E;AA1L7E;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;8CAAkC;AACjC;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;8CAA6B;AAE/B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;iDAAkB;AAG7B;IAAhB,KAAK,EAAE;6CAAuG;AARpG,cAAc;IAD1B,aAAa,CAAC,kBAAkB,CAAC;GACrB,cAAc,CAkT1B","sourcesContent":["import { LitElement, css, html, svg, type TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\n/*\n * vocabulary-guard: allow-file — 이 부품의 `node` 는 **그래프의 정점**이고 트윈의 자리가 아니다.\n * 도메인을 모르는 범용 부품이라 자리·설비라는 용어가 여기 있으면 오히려 틀린다.\n *\n * <ox-relation-view> — **한 항목이 무엇과 얽혀 있나**를 한 장으로 보이는 범용 부품.\n *\n * ── 왜 목록이 아니라 그림인가 ──────────────────────────────────────────────────\n * 칩 목록은 \"무엇이 있나\" 는 답해도 \"어떻게 얽혀 있나\" 는 답하지 못한다. 방향(내가 가리키나,\n * 나를 가리키나)과 관계의 이름이 같은 줄에 섞여 있으면 눈이 그것을 구조로 읽지 못한다.\n * 들어오는 것은 왼쪽, 나가는 것은 오른쪽, 가운데가 나 — 이 배치 하나로 방향이 위치가 된다.\n *\n * ── 이 부품이 도메인을 모른다 ──────────────────────────────────────────────────\n * 축·관계 이름을 알지 않는다. 받는 것은 **일반화된 그래프 조각**(중심 + 방향별 관계 묶음)이고,\n * 무엇을 넣을지는 부르는 쪽이 정한다. 그래서 트윈 인스펙션·런타임 포커스·모델러·AI 답변이\n * 같은 부품을 쓸 수 있고, 관계를 말하는 어휘가 화면마다 나뉘지 않는다.\n *\n * ── 스스로 읽지 않는다 ─────────────────────────────────────────────────────────\n * 질의를 하지 않는다. 데이터를 받고 그리고 **고른 것을 알린다**(`pick`). 안에서 읽기 시작하면\n * 그 순간 특정 서버 계약에 묶여 다른 화면이 못 쓴다.\n *\n * ── 부재를 그린다 ──────────────────────────────────────────────────────────────\n * 이웃이 없는 관계도 **점선 그루터기**로 남긴다. 관계에서 가장 중요한 정보는 대개 부재이고,\n * 없는 것을 안 그리면 \"이 항목은 자리에 안 놓여 있다\" 는 사실이 화면에서 사라진다.\n *\n * ── 규모 ───────────────────────────────────────────────────────────────────────\n * 방향·관계마다 상한을 두고 넘치면 **몇이 남았는지 말한다**. 3만 노드를 한 장에 뿌리지 않는다 —\n * 그건 미루는 일이 아니라 하지 않기로 정하는 일이다. 넘친 것은 `expand` 로 목록에 넘긴다.\n */\n\n/** 그릴 수 있는 노드 하나. `reachable=false` 면 갈 곳이 없다 — 문처럼 보이지 않게 그린다. */\nexport interface RelationNode {\n id: string\n label?: string\n /**\n * 식별자 자리에 **대신 보일 말** — 없으면 `id` 를 보인다.\n *\n * 정체성은 긴 URI 일 수 있지만(`https://…/class/product-B0000080-lot-L26…`) 사람이 대조하는 것은\n * 원본의 코드다. 부르는 쪽이 그 말을 알고 있으므로 여기서 만들지 않고 받는다 — 이 부품은 뜻을\n * 모른 채 자리만 내준다. 전체 식별자는 도구설명에 그대로 남는다(복사·추적의 키다).\n */\n sub?: string\n /** 이 노드가 속한 갈래(축·종류). 부르는 쪽의 어휘 그대로 — 이 부품은 뜻을 알지 않는다. */\n kind?: string\n /**\n * 걸어갈 수 있나. **막다른 링크는 없는 링크보다 나쁘다** — 눌렀는데 아무 일도 안 일어나면\n * 사용자는 그것을 고장으로 받아들인다. 갈 수 없으면 처음부터 문처럼 보이지 않아야 한다.\n */\n reachable?: boolean\n /** 이 이웃이 실제로는 없는 것을 가리킨다(끊어진 참조). 눈에 띄어야 한다. */\n broken?: boolean\n}\n\n/** 한 관계 묶음 — \"무슨 관계로\" + \"어느 방향으로\" 이어진 이웃들. */\nexport interface RelationGroup {\n /** 관계 이름(이미 번역된 표시명). 선 위에 적힌다. */\n via: string\n /** `out` = 내가 가리킨다 · `in` = 나를 가리킨다. */\n direction: 'out' | 'in'\n nodes: RelationNode[]\n /** 전체 건수(상한에 걸려 일부만 왔을 수 있다). 없으면 `nodes.length`. */\n total?: number\n /** 선언은 되어 있는데 이웃이 없다 — 그루터기로 그린다. */\n empty?: boolean\n /** 이 관계가 선택인가 — 비어 있음이 결손인지 정상인지 구분한다. */\n optional?: boolean\n /**\n * 이 이어짐을 **어디서 알았나.**\n *\n * `declared` — 원본이 선언한 관계다(모델이 그렇게 생겼다).\n * `observed` — 이벤트에서 함께 나타났다(실제로 그렇게 움직였다).\n *\n * 둘은 다른 사실이고 섞으면 안 된다. 선언에 없는 이어짐을 선언인 것처럼 그리면 사용자는\n * 모델이 그렇게 되어 있다고 믿게 되고, 반대로 관측된 동반을 안 그리면 실제로 함께 움직이는\n * 것을 놓친다. 그래서 **둘 다 그리되 구별해서** 그린다.\n */\n source?: 'declared' | 'observed'\n /** 관측에서 온 것이면 **어느 범위를 보고 말하는가**(예: 최근 이력 50건). 오류보고없이 자르지 않는다. */\n basis?: string\n /**\n * **아무도 답할 수 없었다** — 「그런 이웃이 없다」와 전혀 다른 사실이다.\n *\n * 비었다는 것은 자료를 채우면 되는 일이고, 못 풀었다는 것은 푸는 쪽을 꽂아야 하는 일이다.\n * 둘을 같은 「—」로 그리면 사람이 엉뚱한 데를 고치러 간다. 값은 부르는 쪽의 말 그대로 받고,\n * 사람이 읽을 문장은 `unresolvedNote` 로 함께 받는다(이 부품은 뜻을 모른다).\n */\n unresolved?: string\n unresolvedNote?: string\n /**\n * 이 이어짐이 **어느 때의 사실인가.**\n *\n * past 이미 일어났다 — 지금 무엇을 해도 안 바뀐다(실적 · 지난 구간)\n * future 아직이다 — 지금 손대면 이것이 걸린다(계획 · 지시)\n * standing 때가 없는 사실(등급 · 종류 · 붙박인 자리)\n * current 지금 그렇고 곧 바뀐다(지금 있는 자리)\n * (없음) **선언이 말하지 않았다.** 줄마다 다르다는 뜻일 수 있다 — 「때가 없다」가 아니다\n *\n * 이것이 왜 한 묶음에 섞이면 안 되나: 설비 하나에 걸린 열 건 중 아홉이 지난 일인 경우와 아홉이\n * 앞의 일인 경우는 **「이 설비를 세워도 되나」의 답이 정반대**다. 세어서 열이라고만 하면 그 답이\n * 화면에서 사라진다.\n *\n * ── 이 값으로 **캐시 여부를 판단하지 않는다** ────────────────────────────\n * 「어느 때가 다시 물어야 하는가」는 계약이 정한다(`isCacheableTense`). 같은 규칙을 화면에도\n * 두면 값이 하나 늘 때 한쪽만 따라간다. 이 부품은 때를 **보여 주기만** 하고, 다시 읽을지는\n * 부르는 쪽이 계약에 물어 정한다.\n */\n tense?: 'past' | 'future' | 'standing' | 'current'\n /** 위 시각을 사람의 말로. 없으면 표식을 그리지 않는다. 이 부품은 문구를 만들지 않는다. */\n tenseLabel?: string\n}\n\n@customElement('ox-relation-view')\nexport class OxRelationView extends LitElement {\n /** 가운데 — 지금 보고 있는 것. */\n @property({ attribute: false }) center: RelationNode = { id: '' }\n @property({ attribute: false }) groups: RelationGroup[] = []\n /** 이웃이 없는 관계도 그루터기로 남길까. 기본은 **남긴다**(부재가 정보다). */\n @property({ type: Boolean }) hideEmpty = false\n\n /** 선을 그리려면 위치를 재야 한다 — 재고 나서 한 번 더 그린다. */\n @state() private edges: { x1: number; y1: number; x2: number; y2: number; label: string; state: string }[] = []\n\n private ro?: ResizeObserver\n\n static styles = css`\n :host { display: block; position: relative; --ox-rv-line: var(--line, #d5dce0); --ox-rv-ink: var(--ink, #17242c);\n --ox-rv-muted: var(--muted, #57676f); --ox-rv-faint: var(--faint, #8a959c);\n --ox-rv-accent: var(--accent, #0d8f86); --ox-rv-warn: var(--warn, #c60); --ox-rv-panel: var(--panel, #fff); }\n\n /*\n * 라벨 자리는 **바깥 여백**으로 낸다.\n *\n * 열에 위쪽 여백을 주면 열 상자만 아래로 밀린다 — 가운데 정렬은 상자를 맞추므로\n * 내용은 그 절반만큼 내려가고, 묶음이 하나뿐일 때도 선이 그만큼 휜다. 여백을 컨테이너에 주면\n * 가운데 노드와 열이 **같이** 밀려 상대 정렬이 그대로 남는다.\n */\n .wrap { position: relative; display: grid; grid-template-columns: 1fr auto 1fr; gap: 26px; align-items: center; padding-top: 15px; }\n /* 좁으면 세로로 배치한다. 그렇게 되면 선이 뜻을 잃으므로 그때는 선을 그리지 않는다. */\n @media (max-width: 720px) { .wrap { grid-template-columns: 1fr; gap: 10px; } }\n\n svg.wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\n svg.wires text { font-family: var(--mono, monospace); font-size: 9.5px; fill: var(--ox-rv-faint); }\n\n .col { display: flex; flex-direction: column; gap: 18px; min-width: 0; }\n .col.in { align-items: flex-end; text-align: right; }\n .col.out { align-items: flex-start; }\n\n /*\n * 묶음의 높이 = **노드 줄의 높이.** 관계 이름은 아이템 위에 얹힌 장식이지 레이아웃 참여자가\n * 아니다. 일반 배치에 두면 그룹의 중심이 라벨만큼 내려가고, 그만큼 선이 휘어 노드에 비스듬히 닿는다\n * — 선을 잘 휘게 만들 일이 아니라 애초에 같은 높이에 두어야 하는 일이다.\n */\n .grp { position: relative; display: flex; max-width: 100%; }\n .grp .via {\n position: absolute; bottom: calc(100% + 2px); white-space: nowrap;\n font-family: var(--mono, monospace); font-size: 9.5px; color: var(--ox-rv-faint); letter-spacing: .03em;\n }\n .col.in .grp .via { right: 0; }\n .col.out .grp .via { left: 0; }\n /* 관측에서 온 이어짐 — 선언과 같은 모양으로 두면 모델이 그렇게 생긴 줄로 읽힌다. */\n .grp .via.obs::before { content: '◦ '; color: var(--ox-rv-accent); }\n .grp .via .basis { opacity: .75; font-style: italic; }\n .grp .row { display: flex; flex-wrap: wrap; gap: 4px; }\n .col.in .grp .row { justify-content: flex-end; }\n\n /*\n * 칩 **안의 글은 왼쪽으로** (2026-08-25 · 사용자 지시). 칩 상자의 자리는 묶음이 정하고(들어오는\n * 쪽은 오른쪽에 붙는다) 그것은 그대로 둔다 — 상자 안에서 이름과 코드가 같은 자리에서 시작해야\n * 여러 줄을 눈으로 훑을 수 있다.\n */\n .node {\n border: 1px solid var(--ox-rv-line); border-radius: 7px; background: var(--ox-rv-panel);\n padding: 3px 9px; font-size: 11.5px; color: var(--ox-rv-accent); cursor: pointer; max-width: 220px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;\n }\n .node:hover { border-color: var(--ox-rv-accent); }\n /* 갈 곳이 없는 값 — 문처럼 보이지 않는다. */\n .node.flat { color: var(--ox-rv-muted); border-style: dashed; cursor: default; }\n .node.broken { color: var(--ox-rv-warn); border-color: color-mix(in oklab, var(--ox-rv-warn), transparent 45%); }\n /* 선언은 있는데 이웃이 없다 — 그루터기. */\n .node.stub { color: var(--ox-rv-faint); border-style: dotted; cursor: default; font-style: italic; }\n .node.stub.req { color: var(--ox-rv-warn); border-color: color-mix(in oklab, var(--ox-rv-warn), transparent 55%); }\n\n /* 못 푼 것 — 비어 있는 것과 **다르게** 보여야 한다. 물음표는 「모른다」이지 「없다」가 아니다. */\n .node.unknown { color: var(--ox-rv-muted); border-style: dashed; border-color: var(--ox-rv-muted); cursor: help; font-style: normal; }\n\n /* 때 표식 — 관계 이름 옆에 붙는 작은 말. 지난 일은 흐리게, 앞의 일은 눈에 걸리게. */\n .grp .via .tense { font-style: normal; letter-spacing: 0; padding: 0 3px; border-radius: 3px; }\n .grp .via .tense[data-t='past'] { color: var(--ox-rv-faint); }\n .grp .via .tense[data-t='future'] { color: var(--ox-rv-warn); }\n .grp .via .tense[data-t='current'] { color: var(--ox-rv-accent); }\n .grp .via .tense[data-t='standing'] { color: var(--ox-rv-muted); }\n\n .more { border: 0; background: transparent; color: var(--ox-rv-accent); font-size: 11px; cursor: pointer; padding: 3px 4px; }\n\n\n .center {\n border: 1.6px solid var(--ox-rv-accent); border-radius: 9px; padding: 7px 13px;\n background: color-mix(in oklab, var(--ox-rv-accent), transparent 92%);\n font-weight: 700; font-size: 13px; color: var(--ox-rv-ink); text-align: center; white-space: nowrap;\n }\n .center .kind { display: block; font-family: var(--mono, monospace); font-weight: 400; font-size: 9.5px; color: var(--ox-rv-faint); }\n /* id 는 이름 아래 작게 — 손잡이이자 동명이인을 구분하는 값이라 지우지 않는다. */\n .center .cid { display: block; font-family: var(--mono, monospace); font-weight: 400; font-size: 10px; color: var(--ox-rv-muted); }\n /*\n * 식별자 줄은 **왼쪽으로 붙인다** (2026-08-25 · 사용자 지시).\n *\n * 이름은 길이가 저마다여서 가운데로 두면 아래의 코드가 줄마다 다른 자리에서 시작한다 — 코드는\n * 눈으로 훑어 대조하는 값이라 시작 자리가 같아야 읽힌다. 이름은 그대로 두고 코드만 붙인다.\n */\n .node .nid { display: block; text-align: left; font-family: var(--mono, monospace); font-size: 9.5px; color: var(--ox-rv-faint); }\n .center .cid { text-align: left; }\n .none { font-size: 11.5px; color: var(--ox-rv-faint); }\n `\n\n connectedCallback(): void {\n super.connectedCallback()\n /* 폭이 바뀌면 선의 좌표가 통째로 달라진다 — 다시 재지 않으면 선이 노드에서 떨어져 나간다. */\n this.ro = new ResizeObserver(() => this.measure())\n this.ro.observe(this)\n }\n\n disconnectedCallback(): void {\n this.ro?.disconnect()\n super.disconnectedCallback()\n }\n\n protected updated(): void {\n this.measure()\n }\n\n /**\n * 선을 잇는다 — **그린 뒤에 재고, 잰 값이 달라졌을 때만 다시 그린다.**\n * 조건 없이 상태를 쓰면 `updated` → `measure` → `updated` 로 무한히 반복된다.\n */\n private measure(): void {\n const root = this.renderRoot as ShadowRoot\n const wrap = root.querySelector('.wrap') as HTMLElement | null\n const centerEl = root.querySelector('.center') as HTMLElement | null\n if (!wrap || !centerEl) return\n\n /* 세로로 접힌 좁은 화면에서는 선이 뜻을 잃는다 — 그럴 때는 아예 그리지 않는다. */\n const stacked = getComputedStyle(wrap).gridTemplateColumns.split(' ').length < 3\n const base = wrap.getBoundingClientRect()\n const c = centerEl.getBoundingClientRect()\n\n const next = stacked\n ? []\n : /*\n * **노드 줄의 중심에서 자기 부모의 중심으로** 잇는다.\n *\n * 그룹 전체(`.grp`)가 아니라 노드 줄(`.row`)을 측정한다. 그룹을 측정하면 위에 얹힌 관계 이름까지\n * 중심에 들어가 선이 상자보다 위로 치우친다.\n */\n /*\n * Array.from 을 쓴다 — 이 꾸러미의 tsconfig 는 NodeList 펼치기를 받지 않는다\n * (downlevelIteration 없음). 트윈에서 옮겨 오면서 여기서만 걸렸다.\n */\n Array.from(root.querySelectorAll<HTMLElement>('.grp .row')).map(row => {\n const g = row.parentElement as HTMLElement\n const a = c\n const r = row.getBoundingClientRect()\n const isIn = !!g.closest('.col.in')\n const gy = r.top - base.top + r.height / 2\n const gx = (isIn ? r.right : r.left) - base.left\n const ax = (isIn ? a.left : a.right) - base.left\n const ay = a.top - base.top + a.height / 2\n return {\n x1: isIn ? gx : ax,\n y1: isIn ? gy : ay,\n x2: isIn ? ax : gx,\n y2: isIn ? ay : gy,\n label: '',\n state: g.dataset.state ?? 'linked'\n }\n })\n\n const same = next.length === this.edges.length && next.every((e, i) => {\n const p = this.edges[i]\n return Math.abs(p.x1 - e.x1) < 0.5 && Math.abs(p.y1 - e.y1) < 0.5 && Math.abs(p.x2 - e.x2) < 0.5 && Math.abs(p.y2 - e.y2) < 0.5 && p.state === e.state\n })\n if (!same) this.edges = next\n }\n\n private pick(n: RelationNode): void {\n if (n.reachable === false) return\n this.dispatchEvent(new CustomEvent('pick', { detail: n, bubbles: true, composed: true }))\n }\n\n private expand(g: RelationGroup): void {\n this.dispatchEvent(new CustomEvent('expand', { detail: g, bubbles: true, composed: true }))\n }\n\n /**\n * 때 순서 — **아직 걸릴 수 있는 것이 위**다.\n *\n * 들어오는 묶음이 여럿일 때 눈이 먼저 닿는 자리에 「앞으로 걸리는 것」을 둔다. 지난 일은 무엇을\n * 해도 안 바뀌므로 아래다. 때를 말하지 않은 것(선언이 줄마다 다르다고 남겨 둔 것)은 가운데에\n * 둔다 — 「없다」가 아니라 「봐야 안다」이므로 지난 일보다 앞이다.\n */\n private static readonly TENSE_ORDER: Record<string, number> = { future: 0, current: 1, standing: 3, past: 4 }\n\n private byTense = (a: RelationGroup, b: RelationGroup) =>\n (OxRelationView.TENSE_ORDER[a.tense ?? ''] ?? 2) - (OxRelationView.TENSE_ORDER[b.tense ?? ''] ?? 2)\n\n render(): TemplateResult {\n /*\n * 「비었다」를 감출 때도 **못 푼 것은 남긴다.** 그것은 없다는 뜻이 아니라 답을 못 얻었다는\n * 뜻이라, 감추면 화면이 「없다」고 말한 것이 된다.\n */\n const shown = this.hideEmpty ? this.groups.filter(g => g.nodes.length || g.unresolved) : this.groups\n const incoming = shown.filter(g => g.direction === 'in').sort(this.byTense)\n const outgoing = shown.filter(g => g.direction === 'out').sort(this.byTense)\n\n return html`\n <div class=\"wrap\">\n <svg class=\"wires\" aria-hidden=\"true\">\n ${this.edges.map(e => svg`\n <path d=\"M${e.x1} ${e.y1} C ${(e.x1 + e.x2) / 2} ${e.y1}, ${(e.x1 + e.x2) / 2} ${e.y2}, ${e.x2} ${e.y2}\"\n fill=\"none\" stroke-width=\"1.2\"\n stroke=${e.state === 'broken' ? 'var(--ox-rv-warn)' : e.state === 'stub' ? 'var(--ox-rv-line)' : 'var(--ox-rv-accent)'}\n stroke-dasharray=${e.state === 'stub' ? '3 3' : e.state === 'observed' ? '1 3' : ''}\n opacity=${e.state === 'stub' ? '0.6' : '0.85'} />`)}\n </svg>\n\n <div class=\"col in\">\n ${incoming.length ? incoming.map(g => this.renderGroup(g)) : html`<span class=\"none\"></span>`}\n </div>\n\n <div class=\"center\">\n ${this.center.label && this.center.label !== this.center.id\n ? html`${this.center.label}<span class=\"cid\">${this.center.sub ?? this.center.id}</span>`\n : this.center.sub ?? this.center.id}\n ${this.center.kind ? html`<span class=\"kind\">${this.center.kind}</span>` : ''}\n </div>\n\n <div class=\"col out\">\n ${outgoing.length ? outgoing.map(g => this.renderGroup(g)) : html`<span class=\"none\"></span>`}\n </div>\n </div>`\n }\n\n /** 한 묶음 — 관계 이름과 그 관계로 이어진 노드들. */\n private renderGroup(g: RelationGroup): TemplateResult {\n const total = g.total ?? g.nodes.length\n const hidden = Math.max(0, total - g.nodes.length)\n const state = g.nodes.some(n => n.broken)\n ? 'broken'\n : !g.nodes.length\n ? /*\n * 수가 있으면 이어져 있는 것이다 — 식별자가 안 왔을 뿐이다. 그루터기(점선)로 그리면\n * 선까지 「없음」이라고 말한다.\n */\n total > 0\n ? 'linked'\n : /* 선언이 「언제나 있다」고 한 자리가 비었으면 그것은 없음이 아니라 **끊어진 참조**다. */\n g.optional === false\n ? 'broken'\n : 'stub'\n : g.source === 'observed'\n ? 'observed'\n : 'linked'\n return html`\n <div class=\"grp\" data-state=${state}>\n <!-- 선언인지 관측인지 밝힌다 — 섞이면 \"모델이 그렇게 생겼다\" 와 \"실제로 그렇게 움직였다\"\n 를 구별할 수 없다. 관측분은 근거 범위도 함께 말한다(오류보고없이 자르지 않는다). -->\n <span class=\"via ${g.source === 'observed' ? 'obs' : ''}\" title=${g.basis ?? ''}\n >${g.via}${g.basis ? html` <span class=\"basis\">${g.basis}</span>` : ''}${g.tenseLabel\n ? html` <span class=\"tense\" data-t=${g.tense ?? ''}>${g.tenseLabel}</span>`\n : ''}</span\n >\n <div class=\"row\">\n ${g.nodes.length\n ? html`\n ${g.nodes.map(n => this.renderNode(n))}\n ${hidden ? html`<button class=\"more\" @click=${() => this.expand(g)}>+${hidden}</button>` : ''}`\n : total > 0\n ? /*\n * ── 수는 있는데 **하나하나가 안 온 것** ──────────────────────\n * 보내는 쪽이 일부러 식별자를 안 보내는 관계가 있다(설비 상태 구간처럼 사람이 하나를\n * 골라 갈 대상이 아닌 것). `ids` 만 보고 그리면 6건이 0건으로 보인다 — 화면이\n * 없는 사실을 말하게 된다. **수를 믿고 목록으로 넘긴다.**\n */\n html`<button class=\"more\" @click=${() => this.expand(g)}>${total}</button>`\n : g.unresolved\n ? /*\n * 못 풀었다. **「없다」로 그리지 않는다** — 사람이 할 일이 다르다(앞은 자료를 채울\n * 일, 뒤는 푸는 쪽을 꽂을 일).\n */\n html`<span class=\"node unknown\" title=${g.unresolvedNote ?? g.unresolved}>?</span>`\n : /* 이웃이 없다 — 선언은 있다. 선택 관계인지에 따라 다르게 읽힌다. */\n html`<span class=\"node stub ${g.optional ? '' : 'req'}\">${g.optional ? '—' : '∅'}</span>`}\n </div>\n </div>`\n }\n\n /**\n * 노드 하나.\n *\n * **제자리에서 더 펼치지 않는다.** 한때 노드마다 자기 이웃을 오른쪽에 이어 그리게 했는데,\n * 양방향 그래프에서는 자식이 거의 언제나 부모를 도로 보여 준다 — 같은 id 가 사방에 되풀이되고\n * 그림이 아니라 털뭉치가 된다. 깊이는 **걸어서** 간다(노드를 누르면 그것이 가운데가 된다).\n * 한 방향으로 여러 단을 봐야 하는 것(BOM 전개)은 되돌아오는 선이 없는 **다른 읽기**다.\n */\n private renderNode(n: RelationNode): TemplateResult {\n /*\n * **이름이 중심이다.** id 만 그리면 그림이 `cutter-op` · `cut-station` 으로 채워져, 관계는\n * 보이는데 무엇과 무엇의 관계인지는 안 보인다. 이름이 있으면 이름을 앞에 세운다.\n *\n * 그래도 id 를 지우지 않는다 — 걷는 손잡이이고, 같은 이름을 가진 것이 둘일 때 구별하는 유일한\n * 값이다. 작게 아래에 둔다(이름이 없으면 id 하나만 남는다).\n */\n const named = !!n.label && n.label !== n.id\n return html`\n <button class=\"node ${n.reachable === false ? 'flat' : ''} ${n.broken ? 'broken' : ''}\"\n ?disabled=${n.reachable === false} title=${n.id} @click=${() => this.pick(n)}\n >${named ? html`${n.label}<span class=\"nid\">${n.sub ?? n.id}</span>` : (n.sub ?? n.id)}</button>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-relation-view': OxRelationView\n }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RelationGroup } from './ox-relation-view.js';
|
|
2
|
+
/**
|
|
3
|
+
* 계약이 내는 이어짐을 **관계 그림이 받는 모양**으로 옮긴다.
|
|
4
|
+
*
|
|
5
|
+
* ## 서버는 판단하지 않는다
|
|
6
|
+
*
|
|
7
|
+
* 서버가 내는 것은 이만큼이다.
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* { via(i18n 키) · target · from · optional · total · ids }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* **번역과 「갈 수 있나」 판단은 화면이 한다.** 서버가 reachable 까지 정해 내려보내면 그 판단이
|
|
14
|
+
* 서버에 굳고, 같은 자료를 다른 방식으로 보여 주는 화면이 그것을 되돌릴 수 없다.
|
|
15
|
+
*
|
|
16
|
+
* 부품도 마찬가지다 — 축 이름도 관계 이름도 모른다. 그래야 런타임 포커스 · 모델러 · AI 답변이
|
|
17
|
+
* 같은 부품을 쓴다. 이 함수가 그 사이를 잇는다.
|
|
18
|
+
*
|
|
19
|
+
* ## 왜 공용인가 (2026-09-05)
|
|
20
|
+
*
|
|
21
|
+
* 트윈의 화면 안에 사유 메서드로 있었다. Plant 가 같은 그림을 쓰려면 이것도 함께 나와야 한다 —
|
|
22
|
+
* 안 나오면 베끼게 되고, 그러면 아래 「타입은 관계가 아니다」 같은 판단이 두 벌이 된다. 두 벌이
|
|
23
|
+
* 되면 한쪽만 고쳐진다.
|
|
24
|
+
*
|
|
25
|
+
* @param links 계약이 낸 이어짐 — `{ out, incoming }`
|
|
26
|
+
* @param nameOf 식별자를 사람이 읽는 이름으로. 모르면 undefined 를 돌려주면 된다 —
|
|
27
|
+
* 부품이 이름 없는 노드로 그린다(label 이 없거나 id 와 같으면 이름 없는 것으로 본다)
|
|
28
|
+
* @param axisLabel 들어오는 쪽의 출처 축을 사람의 말로. 없으면 축 이름을 그대로 쓴다
|
|
29
|
+
*/
|
|
30
|
+
export declare function relationGroups(links: {
|
|
31
|
+
out?: any[];
|
|
32
|
+
incoming?: any[];
|
|
33
|
+
} | undefined, nameOf: (id: string) => string | undefined, axisLabel?: (axis: string) => string): RelationGroup[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { i18next } from '@operato/i18n';
|
|
2
|
+
/**
|
|
3
|
+
* 계약이 내는 이어짐을 **관계 그림이 받는 모양**으로 옮긴다.
|
|
4
|
+
*
|
|
5
|
+
* ## 서버는 판단하지 않는다
|
|
6
|
+
*
|
|
7
|
+
* 서버가 내는 것은 이만큼이다.
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* { via(i18n 키) · target · from · optional · total · ids }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* **번역과 「갈 수 있나」 판단은 화면이 한다.** 서버가 reachable 까지 정해 내려보내면 그 판단이
|
|
14
|
+
* 서버에 굳고, 같은 자료를 다른 방식으로 보여 주는 화면이 그것을 되돌릴 수 없다.
|
|
15
|
+
*
|
|
16
|
+
* 부품도 마찬가지다 — 축 이름도 관계 이름도 모른다. 그래야 런타임 포커스 · 모델러 · AI 답변이
|
|
17
|
+
* 같은 부품을 쓴다. 이 함수가 그 사이를 잇는다.
|
|
18
|
+
*
|
|
19
|
+
* ## 왜 공용인가 (2026-09-05)
|
|
20
|
+
*
|
|
21
|
+
* 트윈의 화면 안에 사유 메서드로 있었다. Plant 가 같은 그림을 쓰려면 이것도 함께 나와야 한다 —
|
|
22
|
+
* 안 나오면 베끼게 되고, 그러면 아래 「타입은 관계가 아니다」 같은 판단이 두 벌이 된다. 두 벌이
|
|
23
|
+
* 되면 한쪽만 고쳐진다.
|
|
24
|
+
*
|
|
25
|
+
* @param links 계약이 낸 이어짐 — `{ out, incoming }`
|
|
26
|
+
* @param nameOf 식별자를 사람이 읽는 이름으로. 모르면 undefined 를 돌려주면 된다 —
|
|
27
|
+
* 부품이 이름 없는 노드로 그린다(label 이 없거나 id 와 같으면 이름 없는 것으로 본다)
|
|
28
|
+
* @param axisLabel 들어오는 쪽의 출처 축을 사람의 말로. 없으면 축 이름을 그대로 쓴다
|
|
29
|
+
*/
|
|
30
|
+
export function relationGroups(links, nameOf, axisLabel = axis => axis) {
|
|
31
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
32
|
+
/*
|
|
33
|
+
* 갈 수 있는지는 여기서 정한다 — 대상이 축이면 그 항목으로 걷고, 아니면 갈 곳이 없다.
|
|
34
|
+
* 이름을 함께 넘긴다. 그림이 식별자로 채워지면 무엇과 무엇의 관계인지 안 보인다.
|
|
35
|
+
*/
|
|
36
|
+
const node = (id, kind) => ({ id, label: nameOf(id), kind, reachable: !!kind });
|
|
37
|
+
/*
|
|
38
|
+
* **타입은 관계가 아니라 속성이다.**
|
|
39
|
+
*
|
|
40
|
+
* 커널이 타입을 관계로 선언한 것은 타당하다 — 타입 어휘로의 참조라 검증에 쓰인다. 하지만 관계
|
|
41
|
+
* **그림**에 그릴 것은 아니다: 다른 개체로 가는 이어짐이 아니라 이 항목의 필드값이고, 갈 곳이
|
|
42
|
+
* 없어 막다른 노드가 되며, 그러면서 진짜 이어짐(가면 뭔가 있는 것)을 옆으로 밀어낸다.
|
|
43
|
+
*
|
|
44
|
+
* ── 계약을 거스르는 것이 아니다. **계약이 스스로 그 구별을 말한다** ──────────
|
|
45
|
+
*
|
|
46
|
+
* target: { kind: 'axis', axis: 'locations' } 다른 개체로 간다
|
|
47
|
+
* target: { kind: 'type', role: 'equipment' } 타입 어휘로의 참조다
|
|
48
|
+
*
|
|
49
|
+
* `kind` 가 그 둘을 갈라 두었으므로, 여기서 `kind !== 'type'` 으로 거르는 것은 **계약이 선언한
|
|
50
|
+
* 성질을 읽는 것**이지 선언을 오류보고없이 무시하는 것이 아니다.
|
|
51
|
+
*
|
|
52
|
+
* 「계약에 있는데 왜 안 그리나」를 묻는 사람이 있을 자리라 적어 둔다 — 답은 「계약이 그것을
|
|
53
|
+
* 타입이라고 말했다」이다. (아키텍트 레인 확인, 2026-09-05)
|
|
54
|
+
*/
|
|
55
|
+
/*
|
|
56
|
+
* ── 바깥 것은 **풀린 만큼만** 말한다 (계약 0.9.7) ──────────────────────────
|
|
57
|
+
*
|
|
58
|
+
* `target.kind === 'external'` 인 관계(자리→구역, 오더·물품→GS1 품목)는 예전에 id 만 왔다.
|
|
59
|
+
* 이제 `refs: [{ id, name?, href?, missing? }]` 로 온다.
|
|
60
|
+
*
|
|
61
|
+
* name 이 없으면 id 를 보인다. **지어내지 않는다**
|
|
62
|
+
* href 가 없으면 선택할 수 없게 둔다. 선택해도 아무 일이 안 일어나면 사람은 고장으로 읽는다
|
|
63
|
+
* missing: true 가리키는데 대상이 없다 — 끊어진 참조로 그린다
|
|
64
|
+
*/
|
|
65
|
+
const externalNodes = (l) => {
|
|
66
|
+
var _a;
|
|
67
|
+
return ((_a = l.refs) !== null && _a !== void 0 ? _a : []).map((ref) => ({
|
|
68
|
+
id: ref.id,
|
|
69
|
+
label: ref.name,
|
|
70
|
+
kind: 'external',
|
|
71
|
+
reachable: !!ref.href,
|
|
72
|
+
broken: !!ref.missing
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
75
|
+
const nodesOf = (l, kindOf) => { var _a; return l.refs ? externalNodes(l) : ((_a = l.ids) !== null && _a !== void 0 ? _a : []).map((id) => node(id, kindOf(l))); };
|
|
76
|
+
/*
|
|
77
|
+
* ── 못 푼 것은 **비어 있는 것과 다르다** ─────────────────────────────────
|
|
78
|
+
* `unresolved` 는 「그런 관계가 없다」가 아니라 「아무도 답할 수 없었다」이다. 앞은 자료를 채울
|
|
79
|
+
* 일이고 뒤는 푸는 쪽을 꽂을 일이라, 화면이 같은 「—」로 그리면 사람이 엉뚱한 데를 고치러 간다.
|
|
80
|
+
*/
|
|
81
|
+
const unresolvedNote = (l) => l.unresolved
|
|
82
|
+
? i18next.t(l.unresolved === 'no-resolver' ? 'text.rel-unresolved-no-resolver' : 'text.rel-unresolved-failed', {
|
|
83
|
+
defaultValue: l.unresolved === 'no-resolver' ? 'no component can resolve this relation' : 'lookup failed'
|
|
84
|
+
})
|
|
85
|
+
: undefined;
|
|
86
|
+
/*
|
|
87
|
+
* ── 때는 **선언이 말한 대로만** 옮긴다 ───────────────────────────────────
|
|
88
|
+
* 말하지 않은 것에 `standing` 을 씌우지 않는다. 미선언은 「때가 없다」가 아니라 「줄마다 다르다」
|
|
89
|
+
* 일 수 있고(작업 줄은 그 줄의 상태를 봐야 안다), 씌우면 판단이 정확히 뒤집힌다.
|
|
90
|
+
*/
|
|
91
|
+
const TENSE_KEY = {
|
|
92
|
+
past: 'text.rel-tense-past',
|
|
93
|
+
future: 'text.rel-tense-future',
|
|
94
|
+
standing: 'text.rel-tense-standing',
|
|
95
|
+
current: 'text.rel-tense-current'
|
|
96
|
+
};
|
|
97
|
+
const TENSE_FALLBACK = {
|
|
98
|
+
past: 'past',
|
|
99
|
+
future: 'upcoming',
|
|
100
|
+
standing: 'standing',
|
|
101
|
+
current: 'current'
|
|
102
|
+
};
|
|
103
|
+
const tenseOf = (l) => {
|
|
104
|
+
const tense = l.tense;
|
|
105
|
+
if (!tense || !TENSE_KEY[tense])
|
|
106
|
+
return {};
|
|
107
|
+
return { tense, tenseLabel: i18next.t(TENSE_KEY[tense], { defaultValue: TENSE_FALLBACK[tense] }) };
|
|
108
|
+
};
|
|
109
|
+
const out = ((_a = links === null || links === void 0 ? void 0 : links.out) !== null && _a !== void 0 ? _a : [])
|
|
110
|
+
.filter((l) => { var _a; return ((_a = l.target) === null || _a === void 0 ? void 0 : _a.kind) !== 'type'; })
|
|
111
|
+
.map((l) => ({
|
|
112
|
+
via: i18next.t(l.via),
|
|
113
|
+
direction: 'out',
|
|
114
|
+
optional: !!l.optional,
|
|
115
|
+
total: l.total,
|
|
116
|
+
unresolved: l.unresolved,
|
|
117
|
+
unresolvedNote: unresolvedNote(l),
|
|
118
|
+
...tenseOf(l),
|
|
119
|
+
nodes: nodesOf(l, (one) => { var _a; return (((_a = one.target) === null || _a === void 0 ? void 0 : _a.kind) === 'axis' ? one.target.axis : undefined); })
|
|
120
|
+
}));
|
|
121
|
+
const incoming = ((_b = links === null || links === void 0 ? void 0 : links.incoming) !== null && _b !== void 0 ? _b : []).map((l) => ({
|
|
122
|
+
via: `${axisLabel(l.from)} · ${i18next.t(l.via)}`,
|
|
123
|
+
direction: 'in',
|
|
124
|
+
optional: !!l.optional,
|
|
125
|
+
total: l.total,
|
|
126
|
+
unresolved: l.unresolved,
|
|
127
|
+
unresolvedNote: unresolvedNote(l),
|
|
128
|
+
...tenseOf(l),
|
|
129
|
+
nodes: nodesOf(l, (one) => one.from)
|
|
130
|
+
}));
|
|
131
|
+
/* 「+N」 을 선택했을 때 목록으로 넘길 항목을 함께 담는다. 부품은 그것을 그대로 돌려준다. */
|
|
132
|
+
for (const g of out) {
|
|
133
|
+
;
|
|
134
|
+
g.axis = ((_e = (_d = ((_c = links === null || links === void 0 ? void 0 : links.out) !== null && _c !== void 0 ? _c : []).find((l) => i18next.t(l.via) === g.via)) === null || _d === void 0 ? void 0 : _d.target) !== null && _e !== void 0 ? _e : {}).axis;
|
|
135
|
+
}
|
|
136
|
+
for (const g of incoming) {
|
|
137
|
+
;
|
|
138
|
+
g.axis = (_g = ((_f = links === null || links === void 0 ? void 0 : links.incoming) !== null && _f !== void 0 ? _f : []).find((l) => g.via.endsWith(i18next.t(l.via)))) === null || _g === void 0 ? void 0 : _g.from;
|
|
139
|
+
}
|
|
140
|
+
return [...incoming, ...out];
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=relation-groups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation-groups.js","sourceRoot":"","sources":["../../../src/relation/relation-groups.ts"],"names":[],"mappings":"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","sourcesContent":["import { i18next } from '@operato/i18n'\n\nimport type { RelationGroup, RelationNode } from './ox-relation-view.js'\n\n/**\n * 계약이 내는 이어짐을 **관계 그림이 받는 모양**으로 옮긴다.\n *\n * ## 서버는 판단하지 않는다\n *\n * 서버가 내는 것은 이만큼이다.\n *\n * ```\n * { via(i18n 키) · target · from · optional · total · ids }\n * ```\n *\n * **번역과 「갈 수 있나」 판단은 화면이 한다.** 서버가 reachable 까지 정해 내려보내면 그 판단이\n * 서버에 굳고, 같은 자료를 다른 방식으로 보여 주는 화면이 그것을 되돌릴 수 없다.\n *\n * 부품도 마찬가지다 — 축 이름도 관계 이름도 모른다. 그래야 런타임 포커스 · 모델러 · AI 답변이\n * 같은 부품을 쓴다. 이 함수가 그 사이를 잇는다.\n *\n * ## 왜 공용인가 (2026-09-05)\n *\n * 트윈의 화면 안에 사유 메서드로 있었다. Plant 가 같은 그림을 쓰려면 이것도 함께 나와야 한다 —\n * 안 나오면 베끼게 되고, 그러면 아래 「타입은 관계가 아니다」 같은 판단이 두 벌이 된다. 두 벌이\n * 되면 한쪽만 고쳐진다.\n *\n * @param links 계약이 낸 이어짐 — `{ out, incoming }`\n * @param nameOf 식별자를 사람이 읽는 이름으로. 모르면 undefined 를 돌려주면 된다 —\n * 부품이 이름 없는 노드로 그린다(label 이 없거나 id 와 같으면 이름 없는 것으로 본다)\n * @param axisLabel 들어오는 쪽의 출처 축을 사람의 말로. 없으면 축 이름을 그대로 쓴다\n */\nexport function relationGroups(\n links: { out?: any[]; incoming?: any[] } | undefined,\n nameOf: (id: string) => string | undefined,\n axisLabel: (axis: string) => string = axis => axis\n): RelationGroup[] {\n /*\n * 갈 수 있는지는 여기서 정한다 — 대상이 축이면 그 항목으로 걷고, 아니면 갈 곳이 없다.\n * 이름을 함께 넘긴다. 그림이 식별자로 채워지면 무엇과 무엇의 관계인지 안 보인다.\n */\n const node = (id: string, kind?: string) => ({ id, label: nameOf(id), kind, reachable: !!kind })\n\n /*\n * **타입은 관계가 아니라 속성이다.**\n *\n * 커널이 타입을 관계로 선언한 것은 타당하다 — 타입 어휘로의 참조라 검증에 쓰인다. 하지만 관계\n * **그림**에 그릴 것은 아니다: 다른 개체로 가는 이어짐이 아니라 이 항목의 필드값이고, 갈 곳이\n * 없어 막다른 노드가 되며, 그러면서 진짜 이어짐(가면 뭔가 있는 것)을 옆으로 밀어낸다.\n *\n * ── 계약을 거스르는 것이 아니다. **계약이 스스로 그 구별을 말한다** ──────────\n *\n * target: { kind: 'axis', axis: 'locations' } 다른 개체로 간다\n * target: { kind: 'type', role: 'equipment' } 타입 어휘로의 참조다\n *\n * `kind` 가 그 둘을 갈라 두었으므로, 여기서 `kind !== 'type'` 으로 거르는 것은 **계약이 선언한\n * 성질을 읽는 것**이지 선언을 오류보고없이 무시하는 것이 아니다.\n *\n * 「계약에 있는데 왜 안 그리나」를 묻는 사람이 있을 자리라 적어 둔다 — 답은 「계약이 그것을\n * 타입이라고 말했다」이다. (아키텍트 레인 확인, 2026-09-05)\n */\n /*\n * ── 바깥 것은 **풀린 만큼만** 말한다 (계약 0.9.7) ──────────────────────────\n *\n * `target.kind === 'external'` 인 관계(자리→구역, 오더·물품→GS1 품목)는 예전에 id 만 왔다.\n * 이제 `refs: [{ id, name?, href?, missing? }]` 로 온다.\n *\n * name 이 없으면 id 를 보인다. **지어내지 않는다**\n * href 가 없으면 선택할 수 없게 둔다. 선택해도 아무 일이 안 일어나면 사람은 고장으로 읽는다\n * missing: true 가리키는데 대상이 없다 — 끊어진 참조로 그린다\n */\n const externalNodes = (l: any): RelationNode[] =>\n (l.refs ?? []).map((ref: any) => ({\n id: ref.id,\n label: ref.name,\n kind: 'external',\n reachable: !!ref.href,\n broken: !!ref.missing\n }))\n\n const nodesOf = (l: any, kindOf: (l: any) => string | undefined): RelationNode[] =>\n l.refs ? externalNodes(l) : (l.ids ?? []).map((id: string) => node(id, kindOf(l)))\n\n /*\n * ── 못 푼 것은 **비어 있는 것과 다르다** ─────────────────────────────────\n * `unresolved` 는 「그런 관계가 없다」가 아니라 「아무도 답할 수 없었다」이다. 앞은 자료를 채울\n * 일이고 뒤는 푸는 쪽을 꽂을 일이라, 화면이 같은 「—」로 그리면 사람이 엉뚱한 데를 고치러 간다.\n */\n const unresolvedNote = (l: any) =>\n l.unresolved\n ? i18next.t(l.unresolved === 'no-resolver' ? 'text.rel-unresolved-no-resolver' : 'text.rel-unresolved-failed', {\n defaultValue: l.unresolved === 'no-resolver' ? 'no component can resolve this relation' : 'lookup failed'\n })\n : undefined\n\n /*\n * ── 때는 **선언이 말한 대로만** 옮긴다 ───────────────────────────────────\n * 말하지 않은 것에 `standing` 을 씌우지 않는다. 미선언은 「때가 없다」가 아니라 「줄마다 다르다」\n * 일 수 있고(작업 줄은 그 줄의 상태를 봐야 안다), 씌우면 판단이 정확히 뒤집힌다.\n */\n const TENSE_KEY: Record<string, string> = {\n past: 'text.rel-tense-past',\n future: 'text.rel-tense-future',\n standing: 'text.rel-tense-standing',\n current: 'text.rel-tense-current'\n }\n const TENSE_FALLBACK: Record<string, string> = {\n past: 'past',\n future: 'upcoming',\n standing: 'standing',\n current: 'current'\n }\n const tenseOf = (l: any) => {\n const tense = l.tense as RelationGroup['tense']\n if (!tense || !TENSE_KEY[tense]) return {}\n return { tense, tenseLabel: i18next.t(TENSE_KEY[tense], { defaultValue: TENSE_FALLBACK[tense] }) }\n }\n\n const out: RelationGroup[] = (links?.out ?? [])\n .filter((l: any) => l.target?.kind !== 'type')\n .map((l: any) => ({\n via: i18next.t(l.via),\n direction: 'out' as const,\n optional: !!l.optional,\n total: l.total,\n unresolved: l.unresolved,\n unresolvedNote: unresolvedNote(l),\n ...tenseOf(l),\n nodes: nodesOf(l, (one: any) => (one.target?.kind === 'axis' ? one.target.axis : undefined))\n }))\n\n const incoming: RelationGroup[] = (links?.incoming ?? []).map((l: any) => ({\n via: `${axisLabel(l.from)} · ${i18next.t(l.via)}`,\n direction: 'in' as const,\n optional: !!l.optional,\n total: l.total,\n unresolved: l.unresolved,\n unresolvedNote: unresolvedNote(l),\n ...tenseOf(l),\n nodes: nodesOf(l, (one: any) => one.from)\n }))\n\n /* 「+N」 을 선택했을 때 목록으로 넘길 항목을 함께 담는다. 부품은 그것을 그대로 돌려준다. */\n for (const g of out) {\n ;(g as any).axis = ((links?.out ?? []).find((l: any) => i18next.t(l.via) === g.via)?.target ?? {}).axis\n }\n for (const g of incoming) {\n ;(g as any).axis = (links?.incoming ?? []).find((l: any) => g.via.endsWith(i18next.t(l.via)))?.from\n }\n\n return [...incoming, ...out]\n}\n"]}
|
|
@@ -74,10 +74,22 @@ let OxSelectorResourceObjectLegacy = class OxSelectorResourceObjectLegacy extend
|
|
|
74
74
|
autofocus
|
|
75
75
|
></ox-search-form>
|
|
76
76
|
|
|
77
|
+
<!--
|
|
78
|
+
.data 를 넘기지 않는다 — 목록은 fetchHandler 가 채운다.
|
|
79
|
+
|
|
80
|
+
그 값은 늘 ZERO_DATA(빈 것)였다. 이 부품에 data 를 주는 부르는 쪽이 없다. 그리스트는
|
|
81
|
+
data 가 설정되면 reset() 하므로(data-grist 의 updated), 이 부품이 **다시 그려지는 순간
|
|
82
|
+
목록이 통째로 사라진다.**
|
|
83
|
+
|
|
84
|
+
오늘은 안 터진다 — 이 부품에 다시 그릴 상태가 없다. 그러나 새 부품(ox-selector-resource-object)
|
|
85
|
+
에 바닥 단추를 고른 상태에 묶자마자 바로 터졌다. 넘길 값이 없으면서 넘기고 있던 것이므로
|
|
86
|
+
여기서도 걷는다. 남겨 두면 다음 사람이 같은 곳에 빠진다.
|
|
87
|
+
|
|
88
|
+
[확인함] 2026-09-05 · 새 부품에서 라디오 클릭 → reset() 1회 → 20건 → 0건
|
|
89
|
+
-->
|
|
77
90
|
<ox-grist
|
|
78
91
|
.mode=${isMobileDevice() ? 'LIST' : 'GRID'}
|
|
79
92
|
.config=${this.config}
|
|
80
|
-
.data=${this.data}
|
|
81
93
|
.fetchHandler=${this.fetchHandler.bind(this)}
|
|
82
94
|
.selectedRecords=${this.selectedRecords}
|
|
83
95
|
>
|