@things-factory/figure-ui 10.1.5 → 10.1.6
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/client/graphql/index.ts +35 -3
- package/client/modeller/figure-ask.ts +5 -5
- package/client/modeller/figure-canvas.ts +1 -1
- package/client/modeller/figure-capabilities.ts +372 -0
- package/client/modeller/figure-history-panel.ts +171 -18
- package/client/modeller/figure-inspector.ts +392 -76
- package/client/modeller/figure-parts.ts +15 -15
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-report.ts +102 -37
- package/client/modeller/figure-settings.ts +20 -20
- package/client/modeller/figure-side.ts +37 -9
- package/client/modeller/figure-source.test.ts +15 -1
- package/client/modeller/figure-source.ts +31 -15
- package/client/modeller/figure-thumbnail.ts +49 -4
- package/client/modeller/maturity.ts +10 -10
- package/client/pages/figure-list-page.ts +152 -66
- package/client/pages/figure-modeller-page.ts +260 -43
- package/client/types.ts +25 -0
- package/client/viewparts/figure-thumb.ts +172 -0
- package/dist-client/graphql/index.d.ts +8 -1
- package/dist-client/graphql/index.js +32 -3
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-ask.js +5 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +1 -1
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +42 -0
- package/dist-client/modeller/figure-capabilities.js +344 -0
- package/dist-client/modeller/figure-capabilities.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +15 -1
- package/dist-client/modeller/figure-history-panel.js +165 -18
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +84 -0
- package/dist-client/modeller/figure-inspector.js +379 -75
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +15 -15
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +18 -1
- package/dist-client/modeller/figure-report.js +105 -37
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +19 -19
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +1 -0
- package/dist-client/modeller/figure-side.js +36 -8
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -8
- package/dist-client/modeller/figure-source.js +16 -7
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.js +40 -4
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/maturity.js +10 -10
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -4
- package/dist-client/pages/figure-list-page.js +150 -63
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +45 -0
- package/dist-client/pages/figure-modeller-page.js +259 -43
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +23 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +29 -0
- package/dist-client/viewparts/figure-thumb.js +168 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/test/i18n-prefix-guard.test.ts +129 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +307 -264
- package/translations/ja.json +275 -264
- package/translations/ko.json +307 -264
- package/translations/ms.json +275 -264
- package/translations/zh.json +275 -264
|
@@ -82,14 +82,6 @@ export interface BoardModel {
|
|
|
82
82
|
figureType?: string
|
|
83
83
|
detailLevel?: DetailLevel
|
|
84
84
|
styleKit?: string
|
|
85
|
-
/**
|
|
86
|
-
* 형상 전체의 기준점.
|
|
87
|
-
*
|
|
88
|
-
* **부품의 `anchor` 와 다른 것이다.** 이쪽은 점(`Vec3`)이고 저쪽은 축별로 어느 면을
|
|
89
|
-
* 붙잡느냐다. 한 형식 안에 같은 낱말이 둘인 것은 좋지 않으나, 이름을 바꾸면 저장된
|
|
90
|
-
* 원본이 따라와야 하므로 여기서 정하지 않는다.
|
|
91
|
-
*/
|
|
92
|
-
anchor?: Vec3
|
|
93
85
|
/**
|
|
94
86
|
* 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.
|
|
95
87
|
*
|
|
@@ -97,6 +89,14 @@ export interface BoardModel {
|
|
|
97
89
|
* 저장하는 것만으로 **조용히 지워지기** 때문이다.
|
|
98
90
|
*/
|
|
99
91
|
animations?: FigureSource['animations']
|
|
92
|
+
/**
|
|
93
|
+
* 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
|
|
94
|
+
*
|
|
95
|
+
* **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가
|
|
96
|
+
* 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐
|
|
97
|
+
* 주나**를 말하는 다른 칸이다.
|
|
98
|
+
*/
|
|
99
|
+
capabilities?: FigureSource['capabilities']
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */
|
|
@@ -159,6 +159,13 @@ export interface PartModel {
|
|
|
159
159
|
anchor?: FigurePart['anchor']
|
|
160
160
|
repeat?: FigurePart['repeat']
|
|
161
161
|
label?: FigurePart['label']
|
|
162
|
+
/**
|
|
163
|
+
* 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.
|
|
164
|
+
*
|
|
165
|
+
* 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,
|
|
166
|
+
* 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.
|
|
167
|
+
*/
|
|
168
|
+
capability?: FigurePart['capability']
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
const RAD_TO_DEG = 180 / Math.PI
|
|
@@ -223,11 +230,11 @@ export function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSou
|
|
|
223
230
|
return withoutEmpty({
|
|
224
231
|
type: board.figureType ?? '',
|
|
225
232
|
base: { x: board.width, y: board.depth, z: board.height },
|
|
226
|
-
anchor: board.anchor,
|
|
227
233
|
detailLevel: board.detailLevel,
|
|
228
234
|
styleKit: board.styleKit,
|
|
229
235
|
parts: parts.map(part => partTo(part, half)),
|
|
230
|
-
animations: board.animations
|
|
236
|
+
animations: board.animations,
|
|
237
|
+
capabilities: board.capabilities
|
|
231
238
|
}) as FigureSource
|
|
232
239
|
}
|
|
233
240
|
|
|
@@ -239,6 +246,13 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
239
246
|
})
|
|
240
247
|
|
|
241
248
|
const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })
|
|
249
|
+
/*
|
|
250
|
+
재질이 하나도 없으면 **칸 자체를 안 적는다.**
|
|
251
|
+
|
|
252
|
+
빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어
|
|
253
|
+
`material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이
|
|
254
|
+
정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.
|
|
255
|
+
*/
|
|
242
256
|
const material = withoutEmpty({
|
|
243
257
|
token: part.token,
|
|
244
258
|
preset: part.preset,
|
|
@@ -262,13 +276,14 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
262
276
|
}),
|
|
263
277
|
segments: part.segments,
|
|
264
278
|
shape: Object.keys(shape).length > 0 ? shape : undefined,
|
|
265
|
-
material,
|
|
279
|
+
material: Object.keys(material).length > 0 ? material : undefined,
|
|
266
280
|
materialSlot: part.materialSlot,
|
|
267
281
|
keepRound: part.keepRound,
|
|
268
282
|
sizing: part.sizing,
|
|
269
283
|
anchor: part.anchor,
|
|
270
284
|
repeat: part.repeat,
|
|
271
|
-
label: part.label
|
|
285
|
+
label: part.label,
|
|
286
|
+
capability: part.capability
|
|
272
287
|
}) as FigurePart
|
|
273
288
|
}
|
|
274
289
|
|
|
@@ -283,8 +298,8 @@ export function fromFigureSource(source: FigureSource): { board: BoardModel; par
|
|
|
283
298
|
figureType: source.type,
|
|
284
299
|
detailLevel: source.detailLevel,
|
|
285
300
|
styleKit: source.styleKit,
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
animations: source.animations,
|
|
302
|
+
capabilities: source.capabilities
|
|
288
303
|
}) as BoardModel
|
|
289
304
|
|
|
290
305
|
return { board, parts: source.parts.map(part => partFrom(part, half)) }
|
|
@@ -333,6 +348,7 @@ function partFrom(part: FigurePart, half: Vec3): PartModel {
|
|
|
333
348
|
sizing: part.sizing,
|
|
334
349
|
anchor: part.anchor,
|
|
335
350
|
repeat: part.repeat,
|
|
336
|
-
label: part.label
|
|
351
|
+
label: part.label,
|
|
352
|
+
capability: part.capability
|
|
337
353
|
}) as PartModel
|
|
338
354
|
}
|
|
@@ -34,11 +34,40 @@ interface Shooter extends HTMLElement {
|
|
|
34
34
|
snapshot?: (width?: number, height?: number) => Promise<string | undefined>
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.
|
|
39
|
+
*
|
|
40
|
+
* **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면
|
|
41
|
+
* 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,
|
|
42
|
+
* 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.
|
|
43
|
+
*
|
|
44
|
+
* 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래
|
|
45
|
+
* `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**
|
|
46
|
+
*/
|
|
38
47
|
function settle(): Promise<void> {
|
|
39
|
-
|
|
48
|
+
const frames = new Promise<void>(resolve =>
|
|
40
49
|
requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))
|
|
41
50
|
)
|
|
51
|
+
|
|
52
|
+
return Promise.race([frames, delay(600)])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function delay(ms: number): Promise<void> {
|
|
56
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 그림을 기다리는 시간의 한도.
|
|
61
|
+
*
|
|
62
|
+
* 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가
|
|
63
|
+
* 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,
|
|
64
|
+
* 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).
|
|
65
|
+
*/
|
|
66
|
+
const PATIENCE = 5000
|
|
67
|
+
|
|
68
|
+
/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */
|
|
69
|
+
function within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {
|
|
70
|
+
return Promise.race([work, delay(ms).then(() => undefined)])
|
|
42
71
|
}
|
|
43
72
|
|
|
44
73
|
export async function captureThumbnail(
|
|
@@ -66,9 +95,25 @@ export async function captureThumbnail(
|
|
|
66
95
|
|
|
67
96
|
try {
|
|
68
97
|
host.appendChild(shooter)
|
|
69
|
-
await settle()
|
|
70
98
|
|
|
71
|
-
|
|
99
|
+
/*
|
|
100
|
+
**한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을
|
|
101
|
+
기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속
|
|
102
|
+
안 생기면 그 줄이 단서가 된다.
|
|
103
|
+
*/
|
|
104
|
+
const shot = await within(
|
|
105
|
+
(async () => {
|
|
106
|
+
await settle()
|
|
107
|
+
return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined
|
|
108
|
+
})(),
|
|
109
|
+
PATIENCE
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (!shot) {
|
|
113
|
+
console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return shot
|
|
72
117
|
} catch (e) {
|
|
73
118
|
console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)
|
|
74
119
|
return undefined
|
|
@@ -70,7 +70,7 @@ export function maturityOf(source: FigureSource, cost: FigureCost, score: Figure
|
|
|
70
70
|
function scale(cost: FigureCost): MaturityAxis {
|
|
71
71
|
const capacity = capacityFrom(cost.groups, cost.triangles)
|
|
72
72
|
if (!capacity) {
|
|
73
|
-
return { key: 'scale', blocked: 'text.maturity-blocked-no-parts' }
|
|
73
|
+
return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
return {
|
|
@@ -79,7 +79,7 @@ function scale(cost: FigureCost): MaturityAxis {
|
|
|
79
79
|
// 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
|
|
80
80
|
ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
|
|
81
81
|
reading: String(capacity.instances),
|
|
82
|
-
because: 'text.maturity-scale-because'
|
|
82
|
+
because: 'figure.text.maturity-scale-because'
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -102,8 +102,8 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
102
102
|
return {
|
|
103
103
|
key: 'consistency',
|
|
104
104
|
ratio: (3 - faults) / 3,
|
|
105
|
-
readingKey: faults === 0 ? 'text.maturity-consistency-clean' : 'text.maturity-consistency-faults',
|
|
106
|
-
because: 'text.maturity-consistency-because'
|
|
105
|
+
readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
|
|
106
|
+
because: 'figure.text.maturity-consistency-because'
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -117,7 +117,7 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
117
117
|
function reuse(score: FigureScore): MaturityAxis {
|
|
118
118
|
const { distinct, parts, saved } = score.reuse
|
|
119
119
|
if (parts === 0) {
|
|
120
|
-
return { key: 'reuse', blocked: 'text.maturity-blocked-no-parts' }
|
|
120
|
+
return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return {
|
|
@@ -125,7 +125,7 @@ function reuse(score: FigureScore): MaturityAxis {
|
|
|
125
125
|
// ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
|
|
126
126
|
ratio: 1 - distinct / parts,
|
|
127
127
|
reading: String(saved),
|
|
128
|
-
because: 'text.maturity-reuse-because'
|
|
128
|
+
because: 'figure.text.maturity-reuse-because'
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -148,7 +148,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
148
148
|
// 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
|
|
149
149
|
ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
|
|
150
150
|
reading: `${cost.parts} / ${limit}`,
|
|
151
|
-
because: 'text.maturity-detail-because'
|
|
151
|
+
because: 'figure.text.maturity-detail-because'
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -162,7 +162,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
162
162
|
* **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
|
|
163
163
|
*/
|
|
164
164
|
function reactive(source: FigureSource): MaturityAxis {
|
|
165
|
-
return { key: 'reactive', blocked: 'text.maturity-blocked-no-binding' }
|
|
165
|
+
return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
@@ -205,7 +205,7 @@ function reactive(source: FigureSource): MaturityAxis {
|
|
|
205
205
|
function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
206
206
|
const regions = topViewRegions(source)
|
|
207
207
|
if (regions.total === 0) {
|
|
208
|
-
return { key: 'readable', blocked: 'text.maturity-blocked-no-parts' }
|
|
208
|
+
return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
|
|
@@ -221,7 +221,7 @@ function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
221
221
|
key: 'readable',
|
|
222
222
|
ratio: Math.min(1, entropy / ceiling),
|
|
223
223
|
reading: `${regions.byMaterial.size}`,
|
|
224
|
-
because: 'text.maturity-readable-because'
|
|
224
|
+
because: 'figure.text.maturity-readable-because'
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@operato/data-grist'
|
|
2
2
|
import '@material/web/icon/icon.js'
|
|
3
|
+
import '../viewparts/figure-thumb.js'
|
|
3
4
|
|
|
4
5
|
import { css, html } from 'lit'
|
|
5
6
|
import { customElement, property, query } from 'lit/decorators.js'
|
|
@@ -35,12 +36,10 @@ const CAPACITY = 'font-weight: 700; color: var(--md-sys-color-primary);'
|
|
|
35
36
|
const UNMEASURED = 'font-style: italic; color: var(--md-sys-color-outline);'
|
|
36
37
|
const RELEASED = 'font-weight: 700; color: var(--md-sys-color-primary);'
|
|
37
38
|
const DRAFT = 'color: var(--md-sys-color-tertiary);'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: 'color: var(--md-sys-color-on-primary-container); background-color: var(--md-sys-color-primary-container);'
|
|
43
|
-
}`
|
|
39
|
+
/** 조용히 적는 숫자. 낮은 것만 점으로 눈에 걸리게 한다. */
|
|
40
|
+
const SCORE = 'display: inline-flex; align-items: center; gap: 4px; font-variant-numeric: tabular-nums;'
|
|
41
|
+
const LOW_DOT =
|
|
42
|
+
'width: 5px; height: 5px; border-radius: 50%; background-color: var(--md-sys-color-error); flex: none;'
|
|
44
43
|
|
|
45
44
|
/*
|
|
46
45
|
* `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는
|
|
@@ -78,6 +77,44 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
78
77
|
width: 100%;
|
|
79
78
|
background-color: var(--md-sys-color-surface);
|
|
80
79
|
color: var(--md-sys-color-on-surface);
|
|
80
|
+
/* 떠 있는 단추의 기준 상자 */
|
|
81
|
+
position: relative;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
── 만들기 단추 ────────────────────────────────────────────────────────
|
|
86
|
+
이 화면의 주된 동작 하나. 목록 위에 떠 있고 자리를 먹지 않는다.
|
|
87
|
+
|
|
88
|
+
글자를 함께 둔다(extended FAB). 아이콘만 두면 처음 온 사람은 + 표가 무엇을 만드는지
|
|
89
|
+
모른다 — 이 화면에서 만들 수 있는 것이 무엇인지가 그 글자다.
|
|
90
|
+
*/
|
|
91
|
+
button[fab] {
|
|
92
|
+
position: absolute;
|
|
93
|
+
right: var(--spacing-large, 12px);
|
|
94
|
+
bottom: var(--spacing-large, 12px);
|
|
95
|
+
z-index: 10;
|
|
96
|
+
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 6px;
|
|
100
|
+
padding: 10px 18px 10px 14px;
|
|
101
|
+
font: var(--label-font, inherit);
|
|
102
|
+
font-size: 0.82rem;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
color: var(--md-sys-color-on-primary-container);
|
|
105
|
+
background-color: var(--md-sys-color-primary-container);
|
|
106
|
+
border: none;
|
|
107
|
+
/* 떠 있는 것의 모서리는 알약이다 — 담는 면(6px)을 따르지 않는다 */
|
|
108
|
+
border-radius: var(--md-sys-shape-corner-full, 999px);
|
|
109
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
}
|
|
112
|
+
button[fab]:hover {
|
|
113
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
114
|
+
background-color: var(--md-sys-color-primary);
|
|
115
|
+
}
|
|
116
|
+
button[fab] md-icon {
|
|
117
|
+
--md-icon-size: 20px;
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
/* 격자가 바깥에 요구하는 것은 확정된 높이 하나뿐이다 — 스크롤은 격자가 안에서 한다 */
|
|
@@ -90,12 +127,18 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
90
127
|
── 그림이 주인공인 카탈로그 ────────────────────────────────────────
|
|
91
128
|
카드 기본값은 글이 많은 목록(직원·결재함)에 맞춰져 있다. 여기서는 사람이 먼저 보는 것이
|
|
92
129
|
형상 그림이므로, 그림 칸을 비율로 잡아 카드 폭이 달라져도 그림이 나란히 읽히게 한다.
|
|
130
|
+
|
|
131
|
+
그림은 카드 끝까지 닿는다 — 위쪽 모서리만 카드와 같은 6px 로 깎아 한 장처럼 보이게 하고,
|
|
132
|
+
그림 안에 얹히는 초안 표시·삭제는 figure-thumb 이 그린다.
|
|
93
133
|
*/
|
|
94
134
|
ox-grist {
|
|
95
135
|
--data-card-min-width: 240px;
|
|
136
|
+
--data-card-record-card-border-radius: 6px;
|
|
137
|
+
--data-card-record-card-padding: 0;
|
|
96
138
|
--data-card-thumbnail-height: auto;
|
|
97
139
|
--data-card-thumbnail-aspect: 4 / 3;
|
|
98
|
-
--data-card-thumbnail-padding:
|
|
140
|
+
--data-card-thumbnail-padding: 0;
|
|
141
|
+
--data-card-thumbnail-radius: 6px 6px 0 0;
|
|
99
142
|
--data-card-thumbnail-background: var(--md-sys-color-surface-container);
|
|
100
143
|
|
|
101
144
|
/*
|
|
@@ -111,6 +154,13 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
111
154
|
|
|
112
155
|
/* 상태와 성숙도는 짧은 표식이다 — 두 줄로 세우지 않고 한 줄에 뉜다 */
|
|
113
156
|
--data-card-detail-direction: row;
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
맨 위로 가는 화살표를 만들기 단추 **위로** 올린다. 둘 다 오른쪽 아래에 떠 있으므로
|
|
160
|
+
그냥 두면 겹친다 — 단추(36px + 아래 여백 12px) 위에 12px 을 띄운 자리다.
|
|
161
|
+
*/
|
|
162
|
+
--data-card-fab-position-vertical: 64px;
|
|
163
|
+
--data-list-fab-position-vertical: 64px;
|
|
114
164
|
}
|
|
115
165
|
`
|
|
116
166
|
]
|
|
@@ -123,25 +173,26 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
123
173
|
|
|
124
174
|
get context() {
|
|
125
175
|
return {
|
|
126
|
-
title: i18next.t('title.figures'),
|
|
176
|
+
title: i18next.t('figure.title.figures'),
|
|
127
177
|
search: {
|
|
128
178
|
handler: (search: string) => {
|
|
129
179
|
this.grist.searchText = search
|
|
130
180
|
},
|
|
131
181
|
value: this.grist?.searchText || '',
|
|
132
|
-
placeholder: i18next.t('text.search-figure-by-name-or-type')
|
|
182
|
+
placeholder: i18next.t('figure.text.search-figure-by-name-or-type')
|
|
133
183
|
},
|
|
134
184
|
filter: {
|
|
135
185
|
handler: () => {
|
|
136
186
|
this.grist.toggleHeadroom()
|
|
137
187
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
}
|
|
189
|
+
/*
|
|
190
|
+
**만들기는 `actions` 가 아니라 FAB 이다.**
|
|
191
|
+
|
|
192
|
+
`context.actions` 로 두면 페이지 바닥 줄에 글자 단추로 서는데, 그 줄은 「지금 화면에 대해
|
|
193
|
+
할 일」을 늘어놓는 자리다. 만들기는 이 화면의 유일한 주된 동작이므로 줄에 섞이지 않고
|
|
194
|
+
떠 있어야 한다. 카드를 가리지 않는 오른쪽 아래, 손이 가는 자리다(아래 render 의 [fab]).
|
|
195
|
+
*/
|
|
145
196
|
}
|
|
146
197
|
}
|
|
147
198
|
|
|
@@ -161,6 +212,10 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
161
212
|
|
|
162
213
|
<ox-grist-personalizer slot="setting"></ox-grist-personalizer>
|
|
163
214
|
</ox-grist>
|
|
215
|
+
|
|
216
|
+
<button fab title=${i18next.t('figure.button.new-figure')} @click=${() => this.openModeller()}>
|
|
217
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.new-figure')}
|
|
218
|
+
</button>
|
|
164
219
|
`
|
|
165
220
|
}
|
|
166
221
|
|
|
@@ -179,44 +234,42 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
179
234
|
*/
|
|
180
235
|
emptyNote: {
|
|
181
236
|
icon: 'deployed_code',
|
|
182
|
-
title: i18next.t('text.no-figure-yet'),
|
|
183
|
-
description: i18next.t('text.what-a-figure-is')
|
|
237
|
+
title: i18next.t('figure.text.no-figure-yet'),
|
|
238
|
+
description: i18next.t('figure.text.what-a-figure-is')
|
|
184
239
|
},
|
|
185
240
|
|
|
186
241
|
list: {
|
|
242
|
+
/* 그림 칸은 그 컬럼의 renderer 가 그린다 — 그림 없을 때의 안내까지 `figure-thumb` 안이다 */
|
|
187
243
|
thumbnail: 'thumbnail',
|
|
188
|
-
/* 그림이 없는 카드도 자리는 지킨다 — 빈 사각형으로 두지 않고 왜 없는지 말한다 */
|
|
189
|
-
emptyThumbnail: {
|
|
190
|
-
icon: 'deployed_code',
|
|
191
|
-
label: i18next.t('text.thumbnail-not-made-yet')
|
|
192
|
-
},
|
|
193
244
|
fields: ['name', 'type'],
|
|
194
245
|
/*
|
|
195
|
-
|
|
246
|
+
── 무게를 모른 채 고르게 두지 않는다 ────────────────────────────────
|
|
247
|
+
이름과 그림만 실으면 저작자는 **이 형상이 무거운지 모른 채** 고른다. 그것을 저작면에
|
|
248
|
+
들어가서야 아는 것은 늦다. 그래서 격자로 옮기기 전 손 카드가 실었던 셋을 그대로 싣는다 —
|
|
249
|
+
몇 개까지 놓을 수 있나(배치 한도) · 성숙도 · 삼각형 수.
|
|
196
250
|
|
|
197
|
-
|
|
198
|
-
|
|
251
|
+
한 줄에 뉜다(`--data-card-detail-direction: row`). 이름표는 달지 않는다 — 값 자체가
|
|
252
|
+
「625개까지」·「삼각형 440개」로 읽히고, 성숙도만 숫자 하나여서 손이 갔을 때 이름이 뜬다.
|
|
199
253
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
값이다). 그 축별 내역과 나머지 숫자는 표 모드의 칸과 저작면에 있다.
|
|
203
|
-
|
|
204
|
-
이름표는 달지 않는다. 성숙도는 색과 숫자로 읽히고, 상태는 발행된 것에 아무 말도 하지
|
|
205
|
-
않는다(그것이 예사다) — 초안일 때만 「초안」이 뜬다.
|
|
254
|
+
상태는 이 줄에 없다. 그림 위 초안 표시가 그 일을 한다(`figure-thumb`) — 발행된 것에는
|
|
255
|
+
아무 말도 하지 않으므로 빈 칸이 생기지 않는다.
|
|
206
256
|
*/
|
|
207
|
-
details: ['
|
|
208
|
-
/* 삭제는 손이 갈 때만 나온다 — 고르려고 보는 자리에 되돌릴 수 없는 조작을 띄우지 않는다 */
|
|
209
|
-
gutters: 'hover'
|
|
257
|
+
details: ['capacity', 'score', 'triangles']
|
|
210
258
|
},
|
|
211
259
|
|
|
212
260
|
columns: [
|
|
213
261
|
{ type: 'gutter', gutterName: 'sequence', fixed: true },
|
|
214
262
|
{
|
|
263
|
+
/*
|
|
264
|
+
표 모드에서만 쓰는 삭제 칸. 카드에서는 `figure-thumb` 이 그림 위에 그리므로
|
|
265
|
+
`forList: false` 로 내린다 — 카드 아래 단추 줄은 감춰도 자리가 남는다.
|
|
266
|
+
*/
|
|
215
267
|
type: 'gutter',
|
|
216
268
|
gutterName: 'button',
|
|
217
269
|
icon: 'delete',
|
|
218
|
-
title: i18next.t('button.delete'),
|
|
270
|
+
title: i18next.t('figure.button.delete-figure'),
|
|
219
271
|
danger: true,
|
|
272
|
+
forList: false,
|
|
220
273
|
width: 40,
|
|
221
274
|
handlers: {
|
|
222
275
|
click: (_columns: any, _data: any, _column: any, record: Figure) => this.removeFigure(record)
|
|
@@ -225,7 +278,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
225
278
|
{
|
|
226
279
|
type: 'string',
|
|
227
280
|
name: 'name',
|
|
228
|
-
header: i18next.t('label.name'),
|
|
281
|
+
header: i18next.t('figure.label.name'),
|
|
229
282
|
record: { editable: false },
|
|
230
283
|
filter: 'search',
|
|
231
284
|
sortable: true,
|
|
@@ -238,7 +291,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
238
291
|
*/
|
|
239
292
|
type: 'string',
|
|
240
293
|
name: 'type',
|
|
241
|
-
header: i18next.t('label.type'),
|
|
294
|
+
header: i18next.t('figure.label.figure-type'),
|
|
242
295
|
record: {
|
|
243
296
|
editable: false,
|
|
244
297
|
renderer: (value: string) => html`<code>${value}</code>`
|
|
@@ -250,7 +303,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
250
303
|
{
|
|
251
304
|
type: 'string',
|
|
252
305
|
name: 'state',
|
|
253
|
-
header: i18next.t('label.state'),
|
|
306
|
+
header: i18next.t('figure.label.state'),
|
|
254
307
|
record: {
|
|
255
308
|
editable: false,
|
|
256
309
|
/*
|
|
@@ -263,25 +316,31 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
263
316
|
*/
|
|
264
317
|
renderer: (value: string, _column: any, record: Figure, _rowIndex: number, field: Element) => {
|
|
265
318
|
if (value !== 'released') {
|
|
266
|
-
return html`<span style=${DRAFT}>${i18next.t('label.draft')}</span>`
|
|
319
|
+
return html`<span style=${DRAFT}>${i18next.t('figure.label.draft')}</span>`
|
|
267
320
|
}
|
|
268
321
|
|
|
269
322
|
/* 표에서는 칸이 비면 안 된다. 카드에서만 입을 다문다 — 그리는 자리가 알려 준다. */
|
|
270
323
|
return field?.tagName === 'OX-CARD-FIELD'
|
|
271
324
|
? ''
|
|
272
325
|
: html`<span style=${RELEASED}
|
|
273
|
-
>${i18next.t('label.released')}
|
|
274
|
-
${record.version ? i18next.t('text.version-number', { version: record.version }) : ''}</span
|
|
326
|
+
>${i18next.t('figure.label.figure-released')}
|
|
327
|
+
${record.version ? i18next.t('figure.text.version-number', { version: record.version }) : ''}</span
|
|
275
328
|
>`
|
|
276
329
|
}
|
|
277
330
|
},
|
|
278
|
-
/*
|
|
331
|
+
/*
|
|
332
|
+
전체·초안·발행됨을 단추로 고른다 — 값이 둘뿐인 조건을 펼침 목록에 넣지 않는다.
|
|
333
|
+
|
|
334
|
+
이름표는 비워 둔다. 단추에 「초안」·「발행됨」이라고 적혀 있으니 그 앞의 「상태」는
|
|
335
|
+
같은 말을 한 번 더 하는 것이고, 걸러 보기 줄에서 자리만 먹는다.
|
|
336
|
+
*/
|
|
279
337
|
filter: {
|
|
280
338
|
type: 'select-buttons',
|
|
281
339
|
operator: 'in',
|
|
340
|
+
label: '',
|
|
282
341
|
options: [
|
|
283
|
-
{ display: i18next.t('label.draft'), value: 'draft' },
|
|
284
|
-
{ display: i18next.t('label.released'), value: 'released' }
|
|
342
|
+
{ display: i18next.t('figure.label.draft'), value: 'draft' },
|
|
343
|
+
{ display: i18next.t('figure.label.figure-released'), value: 'released' }
|
|
285
344
|
]
|
|
286
345
|
},
|
|
287
346
|
width: 120
|
|
@@ -295,8 +354,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
295
354
|
*/
|
|
296
355
|
type: 'string',
|
|
297
356
|
name: 'capacity',
|
|
298
|
-
header: i18next.t('label.
|
|
299
|
-
label: true,
|
|
357
|
+
header: i18next.t('figure.label.placement-limit'),
|
|
300
358
|
sortable: false,
|
|
301
359
|
record: {
|
|
302
360
|
editable: false,
|
|
@@ -304,10 +362,10 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
304
362
|
const capacity = capacityFrom(record.groups ?? 0, record.triangles ?? 0)
|
|
305
363
|
|
|
306
364
|
return capacity
|
|
307
|
-
? html`<span style=${CAPACITY} title=${i18next.t('text.reuse-score-explained')}>
|
|
308
|
-
${i18next.t('text.fits-n-instances', { n: capacity.instances.toLocaleString() })}
|
|
365
|
+
? html`<span style=${CAPACITY} title=${i18next.t('figure.text.reuse-score-explained')}>
|
|
366
|
+
${i18next.t('figure.text.fits-n-instances', { n: capacity.instances.toLocaleString() })}
|
|
309
367
|
</span>`
|
|
310
|
-
: html`<span style=${UNMEASURED}>${i18next.t('text.not-measured-yet')}</span>`
|
|
368
|
+
: html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`
|
|
311
369
|
}
|
|
312
370
|
},
|
|
313
371
|
width: 120
|
|
@@ -315,23 +373,27 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
315
373
|
{
|
|
316
374
|
type: 'float',
|
|
317
375
|
name: 'score',
|
|
318
|
-
header: i18next.t('label.maturity'),
|
|
376
|
+
header: i18next.t('figure.label.maturity'),
|
|
319
377
|
record: {
|
|
320
378
|
editable: false,
|
|
321
379
|
/*
|
|
322
|
-
카드에 남긴 하나뿐인 숫자. **이름표를 달지 않으므로** 무엇인지는
|
|
323
|
-
|
|
380
|
+
카드에 남긴 하나뿐인 숫자. **이름표를 달지 않으므로** 무엇인지는 손이 갔을 때 뜨는
|
|
381
|
+
말이 알려 준다.
|
|
382
|
+
|
|
383
|
+
색칠한 배지로 두지 않는다. 지금 카탈로그의 열넷 중 열셋이 0 점이라(선언된 묶음 한도
|
|
384
|
+
3 을 넘긴다) 붉은 덩어리가 카드마다 서면 목록이 시끄럽고, 그러면서 아무것도 가르지
|
|
385
|
+
못한다. 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.
|
|
324
386
|
|
|
325
|
-
아직 재지 않은 것을
|
|
326
|
-
등급 문자(A~D)도 여기서 만들지
|
|
327
|
-
화면이 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.
|
|
387
|
+
아직 재지 않은 것을 0 으로 보여 주지 않는다 — 0 은 「가볍다」는 뜻이 되어 버린다.
|
|
388
|
+
등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` 가 정하고,
|
|
389
|
+
화면이 그것을 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.
|
|
328
390
|
*/
|
|
329
391
|
renderer: (value: number | null) =>
|
|
330
392
|
value == null
|
|
331
|
-
? html`<span style=${UNMEASURED}>${i18next.t('text.not-measured-yet')}</span>`
|
|
332
|
-
: html`<span style=${
|
|
333
|
-
|
|
334
|
-
>`
|
|
393
|
+
? html`<span style=${UNMEASURED}>${i18next.t('figure.text.not-measured-yet')}</span>`
|
|
394
|
+
: html`<span style=${SCORE} title=${i18next.t('figure.label.maturity')}>
|
|
395
|
+
${value < ATTENTION_BELOW ? html`<i style=${LOW_DOT}></i>` : ''}${Math.round(value)}
|
|
396
|
+
</span>`
|
|
335
397
|
},
|
|
336
398
|
sortable: true,
|
|
337
399
|
width: 90
|
|
@@ -339,11 +401,16 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
339
401
|
{
|
|
340
402
|
type: 'integer',
|
|
341
403
|
name: 'triangles',
|
|
342
|
-
header: i18next.t('label.triangles'),
|
|
343
|
-
label: true,
|
|
404
|
+
header: i18next.t('figure.label.triangles'),
|
|
344
405
|
record: {
|
|
345
406
|
editable: false,
|
|
346
|
-
|
|
407
|
+
/*
|
|
408
|
+
카드에서는 이름표 없이 서므로 숫자만 두면 옆의 성숙도와 구별되지 않는다. 무엇의
|
|
409
|
+
수인지 값이 스스로 말하게 한다 — 표 모드에서는 머리글이 그 일을 하므로 겹치지만,
|
|
410
|
+
그 겹침이 카드에서 뜻을 잃는 것보다 낫다.
|
|
411
|
+
*/
|
|
412
|
+
renderer: (value: number | null) =>
|
|
413
|
+
value == null ? '' : i18next.t('figure.text.triangle-count', { count: value })
|
|
347
414
|
},
|
|
348
415
|
sortable: true,
|
|
349
416
|
width: 110
|
|
@@ -351,16 +418,35 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
351
418
|
{
|
|
352
419
|
type: 'integer',
|
|
353
420
|
name: 'groups',
|
|
354
|
-
header: i18next.t('label.budget-groups'),
|
|
421
|
+
header: i18next.t('figure.label.budget-groups'),
|
|
355
422
|
record: { editable: false },
|
|
356
423
|
hidden: true,
|
|
357
424
|
width: 0
|
|
358
425
|
},
|
|
359
426
|
{
|
|
427
|
+
/*
|
|
428
|
+
**카드의 그림 칸 전체를 이 renderer 가 그린다.**
|
|
429
|
+
|
|
430
|
+
기본 image renderer 는 그림만 그리고, 그 위에 얹혀야 하는 것(초안 표시·삭제)은 카드
|
|
431
|
+
아래 gutter 줄로 밀린다. 격자로 옮기기 전 손 카드가 그것을 그림 위에 두었고 그 편이
|
|
432
|
+
맞다 — 그래서 그림 칸을 `figure-thumb` 에게 넘긴다. hover 가 필요해 컴포넌트여야
|
|
433
|
+
하는 사연은 그 파일에 적혀 있다.
|
|
434
|
+
|
|
435
|
+
`renderEmpty` — 그림이 아직 없는 것도 자리를 지킨다. 그 카드에도 초안 표시와 삭제가
|
|
436
|
+
있어야 하고, 「그림 없음」이라고 말하는 것도 `figure-thumb` 이 한다.
|
|
437
|
+
*/
|
|
360
438
|
type: 'image',
|
|
361
439
|
name: 'thumbnail',
|
|
362
440
|
header: i18next.t('field.thumbnail'),
|
|
363
|
-
record: {
|
|
441
|
+
record: {
|
|
442
|
+
editable: false,
|
|
443
|
+
align: 'center',
|
|
444
|
+
wide: true,
|
|
445
|
+
renderEmpty: true,
|
|
446
|
+
renderer: (_value: unknown, _column: any, record: Figure) => html`
|
|
447
|
+
<figure-thumb .figure=${record} @remove=${() => this.removeFigure(record)}></figure-thumb>
|
|
448
|
+
`
|
|
449
|
+
},
|
|
364
450
|
hidden: true,
|
|
365
451
|
width: 160
|
|
366
452
|
},
|
|
@@ -459,7 +545,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
459
545
|
/** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */
|
|
460
546
|
private async removeFigure(figure: Figure) {
|
|
461
547
|
// 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다
|
|
462
|
-
const message = i18next.t('text.delete-figure-confirm', { name: figure.name })
|
|
548
|
+
const message = i18next.t('figure.text.delete-figure-confirm', { name: figure.name })
|
|
463
549
|
if (!confirm(message)) {
|
|
464
550
|
return
|
|
465
551
|
}
|