@operato/data-grist 10.10.2 → 10.10.3

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 CHANGED
@@ -3,6 +3,15 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ### [10.10.3](https://github.com/hatiolab/operato/compare/v10.10.2...v10.10.3) (2026-09-09)
7
+
8
+
9
+ ### :bug: Bug Fix
10
+
11
+ * **data-grist:** make CARD mode usable for an image-first catalog ([c332d5a](https://github.com/hatiolab/operato/commit/c332d5a131abe991647047731708e43b3a69d9d1))
12
+
13
+
14
+
6
15
  ### [10.10.2](https://github.com/hatiolab/operato/compare/v10.10.1...v10.10.2) (2026-09-07)
7
16
 
8
17
 
@@ -62,6 +62,36 @@ DataCardGutter.styles = [
62
62
  padding: var(--spacing-small);
63
63
  }
64
64
 
65
+ /*
66
+ ── 아이콘만 있는 gutter 는 상자에 담지 않는다 ──────────────────────────
67
+ 위의 별표 규칙은 글자 단추(iconOnly: false)를 위한 것이다. 그것이 아이콘 하나에도 걸리면
68
+ 두 가지가 함께 잘못된다 — 아이콘이 label 글자 크기(0.8rem)로 줄어 13px 짜리 글리프가 되고,
69
+ 그 작은 글리프가 색칠된 상자에 갇혀 답답해 보인다.
70
+
71
+ 아이콘 단추는 아이콘 크기로 그리고, 바탕은 손이 갈 때만 든다.
72
+ */
73
+ md-icon {
74
+ --md-icon-size: var(--data-card-gutter-icon-size, 20px);
75
+ font-size: var(--data-card-gutter-icon-size, 20px);
76
+ line-height: 1;
77
+ padding: var(--spacing-tiny);
78
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
79
+ background-color: transparent;
80
+ border-radius: var(--md-sys-shape-corner-small);
81
+ cursor: pointer;
82
+ }
83
+
84
+ md-icon:hover {
85
+ color: var(--md-sys-color-on-secondary-container);
86
+ background-color: var(--md-sys-color-secondary-container);
87
+ }
88
+
89
+ /* 되돌릴 수 없는 것은 손이 갔을 때 색으로 말한다 — 늘 붉게 두면 목록이 시끄럽다 */
90
+ :host([danger]) md-icon:hover {
91
+ color: var(--md-sys-color-on-error, #fff);
92
+ background-color: var(--md-sys-color-error);
93
+ }
94
+
65
95
  /* button hover 효과는 button 자체에만 — *:hover 로 광범위 적용 시 자식
66
96
  * (span/md-icon) 의 individual hover 가 secondary background 작은 박스를
67
97
  * 그려서 부자연. 자식들은 button hover 의 효과를 상속받기만 한다. */
@@ -1 +1 @@
1
- {"version":3,"file":"data-card-gutter.js","sourceRoot":"","sources":["../../../src/data-card/data-card-gutter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAkC,MAAM,KAAK,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAI/D,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QA4EuB,WAAM,GAAgB,WAAW,CAAA;QACjC,WAAM,GAAiB,WAAW,CAAA;QAClC,aAAQ,GAAW,CAAC,CAAC,CAAA;IAgCnD,CAAC;IA7BC,MAAM;QACJ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,IAAI,CAAA,EAAE,CAAA;QACf,CAAC;QAED,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAC9C,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,CAAA;QAEhC,oEAAoE;QACpE,OAAO,IAAI,CAAA,IAAI,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAA;IACnE,CAAC;IAED,OAAO,CAAC,OAA6B;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;YAE1C,IAAI,UAAU,IAAI,cAAc,EAAE,CAAC;gBACjC,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;YACvC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,CAAA;YACtC,CAAC;YAED,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;IACH,CAAC;;AA5GM,qBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsEF;CACF,AAxEY,CAwEZ;AAE8B;IAA9B,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;6CAAe;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;8CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;8CAAmC;AAClC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gDAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAe;AA/E/B,cAAc;IAD1B,aAAa,CAAC,gBAAgB,CAAC;GACnB,cAAc,CA8G1B","sourcesContent":["import { css, html, LitElement, PropertyValues, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { ZERO_COLUMN, ZERO_RECORD } from '../configure/zero-config.js'\nimport { ColumnConfig, GristRecord } from '../types.js'\n\n@customElement('ox-card-gutter')\nexport class DataCardGutter extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n background-color: transparent;\n align-content: center;\n align-items: center;\n text-overflow: ellipsis;\n }\n\n :host([row-selector]) {\n margin-right: auto;\n }\n\n * {\n border: 0;\n font-size: var(--md-sys-typescale-label-medium-size, 0.8rem);\n line-height: var(--md-sys-typescale-label-medium-size, 0.8rem);\n\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-primary);\n border-radius: var(--data-card-item-btn-border-radius, var(--md-sys-shape-corner-tiny));\n\n padding: var(--spacing-small);\n }\n\n /* button hover 효과는 button 자체에만 — *:hover 로 광범위 적용 시 자식\n * (span/md-icon) 의 individual hover 가 secondary background 작은 박스를\n * 그려서 부자연. 자식들은 button hover 의 효과를 상속받기만 한다. */\n button:hover {\n cursor: pointer;\n color: var(--md-sys-color-on-secondary);\n background-color: var(--md-sys-color-secondary);\n }\n\n :host([danger]) button {\n color: var(--md-sys-color-on-error);\n background-color: var(--md-sys-color-error);\n border-color: var(--md-sys-color-error);\n }\n\n :host([danger]) button:hover {\n color: var(--md-sys-color-error);\n background-color: var(--md-sys-color-on-secondary);\n border-color: var(--md-sys-color-on-secondary);\n }\n\n button {\n display: flex;\n gap: var(--spacing-small);\n border: 0;\n align-items: center;\n }\n\n /* 자식 (span / md-icon) 은 button 의 color 를 inherit — button hover / danger\n * 상태에서 color 가 변하면 자식 글자 / 아이콘 색도 함께 반전. 상위의 *{...}\n * 가 모든 자식에 color 박아서 inherit 안 되던 문제 보수. */\n button > * {\n color: inherit;\n }\n\n button md-icon {\n border: 0;\n padding: 0;\n background-color: unset;\n }\n\n button span {\n padding: 0;\n background-color: transparent;\n }\n `\n ]\n\n @property({ attribute: true }) align?: string\n @property({ type: Object }) record: GristRecord = ZERO_RECORD\n @property({ type: Object }) column: ColumnConfig = ZERO_COLUMN\n @property({ type: Number }) rowIndex: number = -1\n @property({ type: Object }) value?: object\n\n render(): TemplateResult {\n if (!this.column) {\n return html``\n }\n\n var { value, column, record, rowIndex } = this\n var { renderer } = column.record\n\n /* renderer가 html template이 아니고 단순한 값인 경우가 있으므로, html 템플릿으로 감싸준다. */\n return html` ${renderer(value, column, record, rowIndex, this)} `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('column')) {\n const { gutterName, danger } = this.column\n\n if (gutterName == 'row-selector') {\n this.setAttribute('row-selector', '')\n } else {\n this.removeAttribute('row-selector')\n }\n\n if (danger) {\n this.setAttribute('danger', '')\n } else {\n this.removeAttribute('danger')\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"data-card-gutter.js","sourceRoot":"","sources":["../../../src/data-card/data-card-gutter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAkC,MAAM,KAAK,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAI/D,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QA0GuB,WAAM,GAAgB,WAAW,CAAA;QACjC,WAAM,GAAiB,WAAW,CAAA;QAClC,aAAQ,GAAW,CAAC,CAAC,CAAA;IAgCnD,CAAC;IA7BC,MAAM;QACJ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,IAAI,CAAA,EAAE,CAAA;QACf,CAAC;QAED,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAC9C,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,CAAA;QAEhC,oEAAoE;QACpE,OAAO,IAAI,CAAA,IAAI,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAA;IACnE,CAAC;IAED,OAAO,CAAC,OAA6B;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;YAE1C,IAAI,UAAU,IAAI,cAAc,EAAE,CAAC;gBACjC,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;YACvC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,CAAA;YACtC,CAAC;YAED,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;IACH,CAAC;;AA1IM,qBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoGF;CACF,AAtGY,CAsGZ;AAE8B;IAA9B,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;6CAAe;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;8CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;8CAAmC;AAClC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gDAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAe;AA7G/B,cAAc;IAD1B,aAAa,CAAC,gBAAgB,CAAC;GACnB,cAAc,CA4I1B","sourcesContent":["import { css, html, LitElement, PropertyValues, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { ZERO_COLUMN, ZERO_RECORD } from '../configure/zero-config.js'\nimport { ColumnConfig, GristRecord } from '../types.js'\n\n@customElement('ox-card-gutter')\nexport class DataCardGutter extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n background-color: transparent;\n align-content: center;\n align-items: center;\n text-overflow: ellipsis;\n }\n\n :host([row-selector]) {\n margin-right: auto;\n }\n\n * {\n border: 0;\n font-size: var(--md-sys-typescale-label-medium-size, 0.8rem);\n line-height: var(--md-sys-typescale-label-medium-size, 0.8rem);\n\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-primary);\n border-radius: var(--data-card-item-btn-border-radius, var(--md-sys-shape-corner-tiny));\n\n padding: var(--spacing-small);\n }\n\n /*\n ── 아이콘만 있는 gutter 는 상자에 담지 않는다 ──────────────────────────\n 위의 별표 규칙은 글자 단추(iconOnly: false)를 위한 것이다. 그것이 아이콘 하나에도 걸리면\n 두 가지가 함께 잘못된다 — 아이콘이 label 글자 크기(0.8rem)로 줄어 13px 짜리 글리프가 되고,\n 그 작은 글리프가 색칠된 상자에 갇혀 답답해 보인다.\n\n 아이콘 단추는 아이콘 크기로 그리고, 바탕은 손이 갈 때만 든다.\n */\n md-icon {\n --md-icon-size: var(--data-card-gutter-icon-size, 20px);\n font-size: var(--data-card-gutter-icon-size, 20px);\n line-height: 1;\n padding: var(--spacing-tiny);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: transparent;\n border-radius: var(--md-sys-shape-corner-small);\n cursor: pointer;\n }\n\n md-icon:hover {\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n }\n\n /* 되돌릴 수 없는 것은 손이 갔을 때 색으로 말한다 — 늘 붉게 두면 목록이 시끄럽다 */\n :host([danger]) md-icon:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n\n /* button hover 효과는 button 자체에만 — *:hover 로 광범위 적용 시 자식\n * (span/md-icon) 의 individual hover 가 secondary background 작은 박스를\n * 그려서 부자연. 자식들은 button hover 의 효과를 상속받기만 한다. */\n button:hover {\n cursor: pointer;\n color: var(--md-sys-color-on-secondary);\n background-color: var(--md-sys-color-secondary);\n }\n\n :host([danger]) button {\n color: var(--md-sys-color-on-error);\n background-color: var(--md-sys-color-error);\n border-color: var(--md-sys-color-error);\n }\n\n :host([danger]) button:hover {\n color: var(--md-sys-color-error);\n background-color: var(--md-sys-color-on-secondary);\n border-color: var(--md-sys-color-on-secondary);\n }\n\n button {\n display: flex;\n gap: var(--spacing-small);\n border: 0;\n align-items: center;\n }\n\n /* 자식 (span / md-icon) 은 button 의 color 를 inherit — button hover / danger\n * 상태에서 color 가 변하면 자식 글자 / 아이콘 색도 함께 반전. 상위의 *{...}\n * 가 모든 자식에 color 박아서 inherit 안 되던 문제 보수. */\n button > * {\n color: inherit;\n }\n\n button md-icon {\n border: 0;\n padding: 0;\n background-color: unset;\n }\n\n button span {\n padding: 0;\n background-color: transparent;\n }\n `\n ]\n\n @property({ attribute: true }) align?: string\n @property({ type: Object }) record: GristRecord = ZERO_RECORD\n @property({ type: Object }) column: ColumnConfig = ZERO_COLUMN\n @property({ type: Number }) rowIndex: number = -1\n @property({ type: Object }) value?: object\n\n render(): TemplateResult {\n if (!this.column) {\n return html``\n }\n\n var { value, column, record, rowIndex } = this\n var { renderer } = column.record\n\n /* renderer가 html template이 아니고 단순한 값인 경우가 있으므로, html 템플릿으로 감싸준다. */\n return html` ${renderer(value, column, record, rowIndex, this)} `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('column')) {\n const { gutterName, danger } = this.column\n\n if (gutterName == 'row-selector') {\n this.setAttribute('row-selector', '')\n } else {\n this.removeAttribute('row-selector')\n }\n\n if (danger) {\n this.setAttribute('danger', '')\n } else {\n this.removeAttribute('danger')\n }\n }\n }\n}\n"]}
@@ -93,7 +93,12 @@ DataCard.styles = [
93
93
  padding: var(--spacing-medium);
94
94
 
95
95
  display: grid;
96
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
96
+ /*
97
+ 카드 한 장의 최소 폭. 화면마다 실리는 것이 다르므로 값을 밖에서 정할 수 있어야 한다 —
98
+ 그림이 주인공인 목록(자산 카탈로그)은 좁게 여러 장을, 글이 많은 목록은 넓게 몇 장을 놓는다.
99
+ 기본값은 지금까지의 값 그대로다.
100
+ */
101
+ grid-template-columns: repeat(auto-fill, minmax(var(--data-card-min-width, 300px), 1fr));
97
102
  grid-template-rows: min-content;
98
103
  grid-gap: var(--spacing-large);
99
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"data-card.js","sourceRoot":"","sources":["../../../src/data-card/data-card.ts"],"names":[],"mappings":"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","sourcesContent":["import i18next from 'i18next'\nimport '@material/web/icon/icon.js'\nimport '../empty-note.js'\nimport './record-card.js'\n\nimport { PropertyValues, css, html, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { DataManipulator } from '../data-manipulator.js'\nimport { GristRecord } from '../types.js'\n\n@customElement('ox-card')\nexport class DataCard extends DataManipulator {\n static styles = [\n css`\n :host {\n overflow-y: auto;\n padding: var(--spacing-medium);\n\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n grid-template-rows: min-content;\n grid-gap: var(--spacing-large);\n }\n\n [selected-row] {\n background-color: var(--grid-record-selected-background-color);\n }\n\n #upward {\n --md-icon-size: var(--icon-size-large);\n position: absolute;\n top: var(--data-card-fab-position-vertical);\n right: var(--data-card-fab-position-horizontal);\n background-color: rgba(255, 255, 255, 0.7);\n border-radius: var(--md-sys-shape-corner-large);\n color: var(--data-card-fab-color);\n box-shadow: var(--data-card-fab-shadow);\n padding: var(--spacing-small);\n }\n\n slot {\n width: 100%;\n }\n\n ox-empty-note {\n display: block;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n }\n\n [card][emphasized-row],\n [card][emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n @media screen and (max-width: 460px) {\n #setting {\n grid-template-columns: 1fr;\n grid-template-rows: 1fr 1fr;\n }\n }\n `\n ]\n\n @property({ type: Boolean }) empty?: boolean\n\n /** 비었을 때 무슨 말을 하나 — 판단은 data-grist 가 하고 여기서는 그리기만 한다. */\n @property({ type: Object }) emptyNote?: { icon?: string; title: string; description?: string }\n\n @state() private _records: GristRecord[] = []\n @state() private isTop: boolean = false\n\n private _total: number = 0\n private _limit: number = 20\n private _page: number = 1\n\n firstUpdated(changes: PropertyValues<this>) {\n this.isTop = true\n\n /* infinite scrolling */\n this.addEventListener('scroll', e => {\n const totalScrollHeight = this.scrollHeight\n const screenHeight = this.offsetHeight\n const currentScrollTop = this.scrollTop\n\n if (totalScrollHeight <= screenHeight + currentScrollTop + 1) {\n /* 마지막 페이지까지 계속 페이지를 증가시킨다. */\n var lastPage = Math.ceil(this._total / this._limit)\n\n if (this._page < lastPage) {\n this.dispatchEvent(new CustomEvent('attach-page', { bubbles: true, composed: true }))\n }\n }\n\n this.isTop = this.scrollTop == 0\n })\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('config')) {\n this._records = []\n this._page = 1\n }\n\n if (changes.has('data')) {\n this._records = this.data.records\n this._total = this.data.total || 0\n this._limit = this.data.limit || 20\n this._page = this.data.page || 0\n }\n }\n\n render() {\n var { classifier } = this.config.rows\n var records = this._records || []\n\n if (this.config && this.config.rows.appendable) {\n records = [...records, { __dirty__: '+' }]\n }\n\n return html`\n ${records.map((record, rowIndex) => {\n var { emphasized } = classifier.call(null, record, rowIndex) || {}\n\n return html`\n <ox-record-card\n .config=${this.config}\n .data=${this.data}\n .record=${record}\n .rowIndex=${rowIndex}\n .emphasized=${emphasized}\n ?selected-row=${record['__selected__']}\n ?dirty=${record['__dirty__']}\n card\n ></ox-record-card>\n `\n })}\n ${this.empty\n ? html`<ox-empty-note\n icon=${this.emptyNote?.icon ?? nothing}\n title=${this.emptyNote?.title ?? ''}\n description=${this.emptyNote?.description ?? nothing}\n ></ox-empty-note>`\n : html``}\n ${this.isTop\n ? html``\n : html` <md-icon id=\"upward\" @click=${(e: Event) => this.gotoTop(e)}>arrow_upward</md-icon> `}\n `\n }\n\n gotoTop(e: Event) {\n this.scrollTop = 0\n\n e.stopPropagation()\n }\n\n get pullToRefreshTarget() {\n return this\n }\n}\n"]}
1
+ {"version":3,"file":"data-card.js","sourceRoot":"","sources":["../../../src/data-card/data-card.ts"],"names":[],"mappings":"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","sourcesContent":["import i18next from 'i18next'\nimport '@material/web/icon/icon.js'\nimport '../empty-note.js'\nimport './record-card.js'\n\nimport { PropertyValues, css, html, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { DataManipulator } from '../data-manipulator.js'\nimport { GristRecord } from '../types.js'\n\n@customElement('ox-card')\nexport class DataCard extends DataManipulator {\n static styles = [\n css`\n :host {\n overflow-y: auto;\n padding: var(--spacing-medium);\n\n display: grid;\n /*\n 카드 한 장의 최소 폭. 화면마다 실리는 것이 다르므로 값을 밖에서 정할 수 있어야 한다 —\n 그림이 주인공인 목록(자산 카탈로그)은 좁게 여러 장을, 글이 많은 목록은 넓게 몇 장을 놓는다.\n 기본값은 지금까지의 값 그대로다.\n */\n grid-template-columns: repeat(auto-fill, minmax(var(--data-card-min-width, 300px), 1fr));\n grid-template-rows: min-content;\n grid-gap: var(--spacing-large);\n }\n\n [selected-row] {\n background-color: var(--grid-record-selected-background-color);\n }\n\n #upward {\n --md-icon-size: var(--icon-size-large);\n position: absolute;\n top: var(--data-card-fab-position-vertical);\n right: var(--data-card-fab-position-horizontal);\n background-color: rgba(255, 255, 255, 0.7);\n border-radius: var(--md-sys-shape-corner-large);\n color: var(--data-card-fab-color);\n box-shadow: var(--data-card-fab-shadow);\n padding: var(--spacing-small);\n }\n\n slot {\n width: 100%;\n }\n\n ox-empty-note {\n display: block;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n }\n\n [card][emphasized-row],\n [card][emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n @media screen and (max-width: 460px) {\n #setting {\n grid-template-columns: 1fr;\n grid-template-rows: 1fr 1fr;\n }\n }\n `\n ]\n\n @property({ type: Boolean }) empty?: boolean\n\n /** 비었을 때 무슨 말을 하나 — 판단은 data-grist 가 하고 여기서는 그리기만 한다. */\n @property({ type: Object }) emptyNote?: { icon?: string; title: string; description?: string }\n\n @state() private _records: GristRecord[] = []\n @state() private isTop: boolean = false\n\n private _total: number = 0\n private _limit: number = 20\n private _page: number = 1\n\n firstUpdated(changes: PropertyValues<this>) {\n this.isTop = true\n\n /* infinite scrolling */\n this.addEventListener('scroll', e => {\n const totalScrollHeight = this.scrollHeight\n const screenHeight = this.offsetHeight\n const currentScrollTop = this.scrollTop\n\n if (totalScrollHeight <= screenHeight + currentScrollTop + 1) {\n /* 마지막 페이지까지 계속 페이지를 증가시킨다. */\n var lastPage = Math.ceil(this._total / this._limit)\n\n if (this._page < lastPage) {\n this.dispatchEvent(new CustomEvent('attach-page', { bubbles: true, composed: true }))\n }\n }\n\n this.isTop = this.scrollTop == 0\n })\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('config')) {\n this._records = []\n this._page = 1\n }\n\n if (changes.has('data')) {\n this._records = this.data.records\n this._total = this.data.total || 0\n this._limit = this.data.limit || 20\n this._page = this.data.page || 0\n }\n }\n\n render() {\n var { classifier } = this.config.rows\n var records = this._records || []\n\n if (this.config && this.config.rows.appendable) {\n records = [...records, { __dirty__: '+' }]\n }\n\n return html`\n ${records.map((record, rowIndex) => {\n var { emphasized } = classifier.call(null, record, rowIndex) || {}\n\n return html`\n <ox-record-card\n .config=${this.config}\n .data=${this.data}\n .record=${record}\n .rowIndex=${rowIndex}\n .emphasized=${emphasized}\n ?selected-row=${record['__selected__']}\n ?dirty=${record['__dirty__']}\n card\n ></ox-record-card>\n `\n })}\n ${this.empty\n ? html`<ox-empty-note\n icon=${this.emptyNote?.icon ?? nothing}\n title=${this.emptyNote?.title ?? ''}\n description=${this.emptyNote?.description ?? nothing}\n ></ox-empty-note>`\n : html``}\n ${this.isTop\n ? html``\n : html` <md-icon id=\"upward\" @click=${(e: Event) => this.gotoTop(e)}>arrow_upward</md-icon> `}\n `\n }\n\n gotoTop(e: Event) {\n this.scrollTop = 0\n\n e.stopPropagation()\n }\n\n get pullToRefreshTarget() {\n return this\n }\n}\n"]}
@@ -59,9 +59,13 @@ let RecordCard = class RecordCard extends LitElement {
59
59
  this.addEventListener('show-record-view', () => this.popupRecordView());
60
60
  }
61
61
  updated(changes) {
62
+ var _a, _b;
62
63
  if (changes.has('record') && this._recordView) {
63
64
  this._recordView.record = this.record;
64
65
  }
66
+ if (changes.has('config')) {
67
+ this.toggleAttribute('gutters-hover', ((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.list) === null || _b === void 0 ? void 0 : _b.gutters) == 'hover');
68
+ }
65
69
  const emphasized = this.emphasized;
66
70
  if (!!emphasized) {
67
71
  this.setAttribute('emphasized-row', '');
@@ -79,7 +83,7 @@ let RecordCard = class RecordCard extends LitElement {
79
83
  const record = this.record;
80
84
  const rowIndex = this.rowIndex;
81
85
  const { columns, list } = this.config;
82
- const { thumbnail, fields, details } = list || {};
86
+ const { thumbnail, fields, details, emptyThumbnail } = list || {};
83
87
  const briefFields = fields.map(field => columns.find(column => column.name == field)).filter(column => column) || [];
84
88
  const detailFields = details.map(field => columns.find(column => column.name == field)).filter(column => column) || [];
85
89
  const thumbnailColumn = thumbnail ? columns.find(column => column.name == thumbnail) : undefined;
@@ -111,14 +115,19 @@ let RecordCard = class RecordCard extends LitElement {
111
115
  }
112
116
  return html `
113
117
  ${dirtyIcon ? html ` <md-icon dirty>${dirtyIcon}</md-icon> ` : html ``}
114
- ${thumbnailColumn && hasThumbnail
115
- ? html ` <ox-card-field
116
- thumbnail
117
- .rowIndex=${rowIndex}
118
- .column=${thumbnailColumn}
119
- .record=${record}
120
- .value=${record[(thumbnailColumn === null || thumbnailColumn === void 0 ? void 0 : thumbnailColumn.name) || '']}
121
- ></ox-card-field>`
118
+ ${thumbnailColumn
119
+ ? hasThumbnail
120
+ ? html ` <ox-card-field
121
+ thumbnail
122
+ .rowIndex=${rowIndex}
123
+ .column=${thumbnailColumn}
124
+ .record=${record}
125
+ .value=${record[(thumbnailColumn === null || thumbnailColumn === void 0 ? void 0 : thumbnailColumn.name) || '']}
126
+ ></ox-card-field>`
127
+ : html `<div thumbnail empty>
128
+ ${(emptyThumbnail === null || emptyThumbnail === void 0 ? void 0 : emptyThumbnail.icon) ? html `<md-icon>${emptyThumbnail.icon}</md-icon>` : ''}
129
+ ${(emptyThumbnail === null || emptyThumbnail === void 0 ? void 0 : emptyThumbnail.label) || ''}
130
+ </div>`
122
131
  : html ``}
123
132
 
124
133
  <div content>
@@ -230,8 +239,24 @@ RecordCard.styles = [
230
239
  }
231
240
 
232
241
  [thumbnail] {
233
- height: var(--data-card-thumbnail-height);
234
- padding: var(--spacing-large);
242
+ /*
243
+ 높이 대신 **비율**로 잡을 수 있다.
244
+
245
+ 그림이 자산인 목록에서는 카드 폭이 달라져도 그림 칸의 비율이 같아야 나란히 읽힌다. 높이만
246
+ 있으면 좁은 카드에서는 납작하고 넓은 카드에서는 홀쭉해진다. 둘 다 안 주면 지금까지처럼
247
+ 내용 높이를 따른다.
248
+ */
249
+ height: var(--data-card-thumbnail-height, auto);
250
+ aspect-ratio: var(--data-card-thumbnail-aspect, auto);
251
+ /*
252
+ 그림 안쪽 여백과 바탕도 밖에서 정한다.
253
+
254
+ 목록마다 그림이 하는 일이 다르다. 사람 얼굴(직원 목록)은 여백이 있어야 카드 안에 놓인
255
+ 것처럼 보이고, 자산 카탈로그의 그림은 **그림이 주인공**이라 여백이 그림을 작게 만든다.
256
+ 바탕도 마찬가지다 — 배경이 없는 그림(투명 png · 3D 미리보기)은 놓일 면이 있어야 한다.
257
+ */
258
+ padding: var(--data-card-thumbnail-padding, var(--spacing-large));
259
+ background-color: var(--data-card-thumbnail-background, transparent);
235
260
  box-sizing: border-box;
236
261
  /* 동심 안쪽 radius — Material 3 shape token (외곽 = corner-medium, 안쪽 =
237
262
  * corner-small). board-ui 카드와 동일 톤. */
@@ -239,6 +264,26 @@ RecordCard.styles = [
239
264
  overflow: hidden;
240
265
  }
241
266
 
267
+ /*
268
+ 그림이 없는 것도 **자리는 지킨다.**
269
+
270
+ 전에는 값이 없으면 썸네일 요소를 아예 그리지 않았다. 그러면 한 목록 안에서 카드 높이가
271
+ 들쭉날쭉해져 눈이 줄을 못 잡고, 「아직 그림이 없다」는 사실을 말할 자리도 사라진다.
272
+ 빈 자리는 조용한 바탕으로 두고, 무엇이라고 부를지는 쓰는 쪽이 정한다.
273
+ */
274
+ [thumbnail][empty] {
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ background-color: var(--data-card-thumbnail-empty-background, var(--md-sys-color-surface-container));
279
+ color: var(--data-card-thumbnail-empty-color, var(--md-sys-color-outline));
280
+ font: var(--data-card-item-etc-font);
281
+ }
282
+
283
+ [thumbnail][empty] md-icon {
284
+ --md-icon-size: 28px;
285
+ }
286
+
242
287
  [content] {
243
288
  display: flex;
244
289
  flex-direction: column;
@@ -253,6 +298,29 @@ RecordCard.styles = [
253
298
  overflow: hidden;
254
299
  }
255
300
 
301
+ /*
302
+ 곁들이는 칸들 — 이 자리에 규칙이 아예 없었다.
303
+
304
+ details 는 「이름 : 값」이 여러 줄 서는 자리인데 그 줄들을 세우는 규칙이 없어서, 카드가
305
+ 글자 크기에 따라 줄이 붙어 버리고 제목 묶음(brief)과도 구분되지 않았다. 세로로 세우고
306
+ 간격을 준다 — 카드에서 「무엇인가」와 「어떤가」는 다른 묶음이다.
307
+
308
+ 선으로 가르지는 않는다. 값이 없는 줄이 섞이는 목록(있을 때만 말하는 칸)에서는 선 아래에
309
+ 아무것도 없는 카드가 생긴다.
310
+
311
+ 「이름 : 값」이 아니라 짧은 표식 몇 개(상태 · 점수)를 다는 목록에서는 세로로 세우는 것이
312
+ 자리만 먹는다. 그때는 화면이 한 줄로 뉘일 수 있다(--data-card-detail-direction: row).
313
+ */
314
+ [detail] {
315
+ display: flex;
316
+ flex-direction: var(--data-card-detail-direction, column);
317
+ flex-wrap: wrap;
318
+ align-items: baseline;
319
+ gap: var(--spacing-tiny) var(--spacing-medium);
320
+ margin-top: var(--spacing-small);
321
+ overflow: hidden;
322
+ }
323
+
256
324
  [gutters] {
257
325
  display: flex;
258
326
  flex-direction: row;
@@ -262,6 +330,42 @@ RecordCard.styles = [
262
330
  align-items: stretch;
263
331
  }
264
332
 
333
+ /*
334
+ ── 손이 갈 때만 나오는 단추 ────────────────────────────────────────
335
+ 단추가 그 카드의 주된 할 일인 목록(결재함의 「상세정보」)에서는 늘 보여야 한다. 반대로
336
+ 고르는 것이 주된 일인 목록(자산 카탈로그)에서 삭제 단추가 카드마다 떠 있으면, 사람이
337
+ 고르려고 보는 자리에 되돌릴 수 없는 조작이 먼저 눈에 든다.
338
+
339
+ 어느 쪽인지는 화면이 정한다(list.gutters: 'hover'). 눌리지 않게 함께 막아야 한다 —
340
+ 보이지 않는 단추가 눌리는 것이 더 나쁘다.
341
+ */
342
+ /*
343
+ 감춘 단추는 **자리도 차지하지 않는다.** opacity 만 0 으로 두면 카드마다 빈 줄이 남아,
344
+ 늘 보이는 것보다 되레 카드가 길어진다. 그림 위 오른쪽 끝으로 띄운다 — 손이 갈 때만
345
+ 나오는 단추가 있을 자리다. 그림이 어두울 수 있으니 옅은 바탕을 깐다.
346
+ */
347
+ :host([gutters-hover]) [gutters] {
348
+ position: absolute;
349
+ top: var(--spacing-small);
350
+ right: var(--spacing-small);
351
+ margin: var(--spacing-none);
352
+ padding: 1px;
353
+ border-radius: var(--md-sys-shape-corner-small);
354
+ background-color: var(
355
+ --data-card-gutters-floating-background,
356
+ color-mix(in srgb, var(--md-sys-color-surface, #fff) 78%, transparent)
357
+ );
358
+ opacity: 0;
359
+ pointer-events: none;
360
+ transition: opacity 0.12s ease-out;
361
+ }
362
+
363
+ :host([gutters-hover]:hover) [gutters],
364
+ :host([gutters-hover]:focus-within) [gutters] {
365
+ opacity: 1;
366
+ pointer-events: auto;
367
+ }
368
+
265
369
  ox-card-field {
266
370
  font: var(--data-card-item-etc-font);
267
371
  color: var(--data-card-item-etc-color, var(--md-sys-color-on-surface));
@@ -1 +1 @@
1
- {"version":3,"file":"record-card.js","sourceRoot":"","sources":["../../../src/data-card/record-card.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../record-view/record-view.js'\nimport './data-card-field.js'\nimport './data-card-gutter.js'\nimport './data-card-gutter-menu.js'\n\nimport { LitElement, PropertyValues, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { GristConfig, GristData, GristRecord } from '../types.js'\nimport { ZERO_CONFIG, ZERO_DATA, ZERO_RECORD } from '../configure/zero-config.js'\nimport { RecordViewHandler } from '../record-view/record-view-handler.js'\nimport { recordCardClickHandler } from './event-handlers/record-card-click-handler.js'\nimport { recordCardDblClickHandler } from './event-handlers/record-card-dblclick-handler.js'\n\n// TODO 로케일 설정에 따라서 포맷이 바뀌도록 해야한다.\nconst OPTIONS: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n hour12: false\n // timeZone: 'America/Los_Angeles'\n}\n\nfunction getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {\n try {\n const safeLocale = locale || 'en-US'\n return new Intl.DateTimeFormat(safeLocale, options)\n } catch (e) {\n return new Intl.DateTimeFormat('en-US', options)\n }\n}\n\nconst formatter = getSafeFormatter(navigator.language, OPTIONS)\n\n@customElement('ox-record-card')\nexport class RecordCard extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n position: relative;\n background-color: var(--data-card-record-card-background-color);\n /* Theme 정의가 누락된 application 에서도 합리적 radius 보장. */\n border-radius: var(--data-card-record-card-border-radius, var(--md-sys-shape-corner-medium));\n border: var(--data-card-record-card-border);\n height: min-content;\n padding: var(--spacing-small) var(--spacing-none);\n }\n\n :host(:hover) {\n background-color: var(--md-sys-color-surface);\n border: var(--data-card-record-card-border-hover);\n box-shadow: var(--data-card-record-card-boxshadow-hover);\n }\n\n :host([dirty])::before {\n content: '';\n position: absolute;\n left: var(--spacing-none);\n top: var(--spacing-none);\n\n width: var(--spacing-none);\n height: var(--spacing-none);\n border-top: var(--grid-record-dirty-border-top);\n border-right: var(--grid-record-dirty-border-left);\n }\n\n :host[emphasized-row] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n [dirty] {\n position: absolute;\n margin: var(--spacing-none);\n height: 20px;\n\n font: var(--grid-record-dirty-icon-font);\n text-indent: 1px;\n left: var(--spacing-none);\n top: var(--spacing-none);\n color: var(--grid-record-dirty-color, var(--md-sys-color-error));\n }\n\n [thumbnail] {\n height: var(--data-card-thumbnail-height);\n padding: var(--spacing-large);\n box-sizing: border-box;\n /* 동심 안쪽 radius — Material 3 shape token (외곽 = corner-medium, 안쪽 =\n * corner-small). board-ui 카드와 동일 톤. */\n border-radius: var(--data-card-thumbnail-radius, var(--md-sys-shape-corner-small));\n overflow: hidden;\n }\n\n [content] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-tiny);\n margin: var(--data-card-item-margin);\n }\n\n [brief] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-tiny);\n overflow: hidden;\n }\n\n [gutters] {\n display: flex;\n flex-direction: row;\n gap: var(--spacing-small);\n margin: var(--spacing-small) var(--spacing-medium);\n justify-content: end;\n align-items: stretch;\n }\n\n ox-card-field {\n font: var(--data-card-item-etc-font);\n color: var(--data-card-item-etc-color, var(--md-sys-color-on-surface));\n }\n\n ox-card-field[name] {\n font: var(--data-card-item-name-font);\n color: var(--data-card-item-name-color, var(--md-sys-color-secondary));\n }\n\n ox-card-field[desc] {\n font: var(--data-card-item-disc-font);\n color: var(--data-card-item-disc-color, var(--md-sys-color-tertiary));\n }\n `\n ]\n\n @property({ type: Object }) config: GristConfig = ZERO_CONFIG\n @property({ type: Object }) data: GristData = ZERO_DATA\n @property({ type: Object }) record: GristRecord = ZERO_RECORD\n @property({ type: Number }) rowIndex: number = -1\n /*\n * row-selector를 사용자가 변경할 때, record-card의 update를 유도하기 위해 selected-row attribute를 property에 추가함.\n * (이를 해주지 않으면, 리스트 refresh 경우에 selected-row checkbox가 클리어되지 않는 현상이 발생함.)\n */\n @property({ type: Boolean, attribute: 'selected-row' }) selectedRow: boolean = false\n @property({ attribute: false }) emphasized: any = false\n\n private _recordView: any\n private clickHandler = recordCardClickHandler.bind(this) as EventListener\n private dblclickHandler = recordCardDblClickHandler.bind(this) as EventListener\n\n firstUpdated() {\n /*\n long-press\n TODO. performance를 확인한 후에 활성화하자.\n */\n // longpressable(this.renderRoot.querySelector('[content]'))\n // this.renderRoot.addEventListener('long-press', recordPartialLongPressHandler.bind(this))\n this.renderRoot.addEventListener('click', this.clickHandler)\n this.renderRoot.addEventListener('dblclick', this.dblclickHandler)\n\n this.addEventListener('show-record-view', () => this.popupRecordView())\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('record') && this._recordView) {\n this._recordView.record = this.record\n }\n\n const emphasized = this.emphasized\n\n if (!!emphasized) {\n this.setAttribute('emphasized-row', '')\n\n if (Symbol.iterator in Object(emphasized)) {\n const [backgroundColor, foregroundColor] = emphasized as string[]\n backgroundColor && this.style.setProperty('--grid-record-emphasized-background-color', backgroundColor)\n foregroundColor && this.style.setProperty('--grid-record-emphasized-color', foregroundColor)\n }\n } else {\n this.removeAttribute('emphasized-row')\n }\n }\n\n render() {\n const record = this.record\n const rowIndex = this.rowIndex\n const { columns, list } = this.config\n const { thumbnail, fields, details } = list || {}\n\n const briefFields = fields.map(field => columns.find(column => column.name == field)).filter(column => column) || []\n const detailFields =\n details.map(field => columns.find(column => column.name == field)).filter(column => column) || []\n\n const thumbnailColumn = thumbnail ? columns.find(column => column.name == thumbnail) : undefined\n /*\n * 값이 없으면 썸네일 자리를 만들지 않는다.\n *\n * 전에는 설정에 썸네일 컬럼 이름이 있으면 **레코드에 값이 있는지 보지 않고** 그렸다. 그래서 이미지를\n * 갖지 않는 목록(결재함 · 업무함)에서 카드마다 깨진 이미지 표시가 세로의 절반을 차지하고, 정작\n * 제목은 잘렸다. 없는 것의 자리를 비워 두는 것이다.\n */\n const thumbnailValue = thumbnailColumn ? record?.[thumbnailColumn.name] : undefined\n const hasThumbnail = Array.isArray(thumbnailValue)\n ? thumbnailValue.length > 0\n : thumbnailValue !== null && thumbnailValue !== undefined && thumbnailValue !== ''\n\n const gutters = (columns || []).filter(column => column.type == 'gutter' && column.forList)\n\n if (this.hasAttribute('dirty')) {\n var dirtyIcon\n\n switch (this.record['__dirty__']) {\n case 'M':\n dirtyIcon = 'done'\n break\n case '+':\n dirtyIcon = 'add'\n break\n case '-':\n dirtyIcon = 'remove'\n break\n }\n }\n\n return html`\n ${dirtyIcon ? html` <md-icon dirty>${dirtyIcon}</md-icon> ` : html``}\n ${thumbnailColumn && hasThumbnail\n ? html` <ox-card-field\n thumbnail\n .rowIndex=${rowIndex}\n .column=${thumbnailColumn}\n .record=${record}\n .value=${record[thumbnailColumn?.name || '']}\n ></ox-card-field>`\n : html``}\n\n <div content>\n <div brief>\n ${briefFields.map(\n (column, idx) => html`\n <ox-card-field\n .rowIndex=${rowIndex}\n .column=${column}\n .record=${record}\n .value=${record[column?.name || '']}\n ?name=${idx == 0}\n ?desc=${idx == 1}\n ></ox-card-field>\n `\n )}\n </div>\n ${detailFields.length > 0\n ? html`\n <div detail>\n ${detailFields.map(\n (column, idx) => html`\n <ox-card-field\n .rowIndex=${rowIndex}\n .column=${column}\n .record=${record}\n .value=${record[column?.name || '']}\n ></ox-card-field>\n `\n )}\n </div>\n `\n : html``}\n </div>\n\n <div gutters>\n ${gutters.map(gutter => {\n return html`\n <ox-card-gutter\n .rowIndex=${rowIndex}\n .column=${gutter}\n .record=${record}\n .value=${record[gutter.name]}\n ></ox-card-gutter>\n `\n })}\n </div>\n `\n }\n\n popupRecordView() {\n var titleField = this.config.list.fields[0] || 'name'\n var title = this.record[titleField]\n\n /* field가 오브젝트형인 경우에는 렌더러를 타이틀로 사용한다. */\n if (typeof title == 'object') {\n var column = this.config.columns.find(column => column.name == titleField)\n title = column?.record.renderer(title, column, this.record, this.rowIndex, this /* cautious */)\n }\n\n this._recordView = RecordViewHandler(\n this.config.columns,\n this.record,\n this.rowIndex,\n this,\n {\n title\n },\n () => {\n delete this._recordView\n }\n )\n }\n}\n"]}
1
+ {"version":3,"file":"record-card.js","sourceRoot":"","sources":["../../../src/data-card/record-card.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,+BAA+B,CAAA;AACtC,OAAO,sBAAsB,CAAA;AAC7B,OAAO,uBAAuB,CAAA;AAC9B,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,UAAU,EAAkB,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAG3D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAA;AACtF,OAAO,EAAE,yBAAyB,EAAE,MAAM,kDAAkD,CAAA;AAE5F,kCAAkC;AAClC,MAAM,OAAO,GAA+B;IAC1C,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,GAAG,EAAE,SAAS;IACd,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,KAAK;IACb,kCAAkC;CACnC,CAAA;AAED,SAAS,gBAAgB,CAAC,MAAc,EAAE,OAAmC;IAC3E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,IAAI,OAAO,CAAA;QACpC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IACrD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAClD,CAAC;AACH,CAAC;AAED,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;AAGxD,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QAoMuB,WAAM,GAAgB,WAAW,CAAA;QACjC,SAAI,GAAc,SAAS,CAAA;QAC3B,WAAM,GAAgB,WAAW,CAAA;QACjC,aAAQ,GAAW,CAAC,CAAC,CAAA;QACjD;;;WAGG;QACqD,gBAAW,GAAY,KAAK,CAAA;QACpD,eAAU,GAAQ,KAAK,CAAA;QAG/C,iBAAY,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAkB,CAAA;QACjE,oBAAe,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAkB,CAAA;IAwKjF,CAAC;IAtKC,YAAY;QACV;;;UAGE;QACF,4DAA4D;QAC5D,2FAA2F;QAC3F,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QAC5D,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;QAElE,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,CAAC,OAA6B;;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC9C,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QACvC,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,eAAe,CAAC,eAAe,EAAE,CAAA,MAAA,MAAA,IAAI,CAAC,MAAM,0CAAE,IAAI,0CAAE,OAAO,KAAI,OAAO,CAAC,CAAA;QAC9E,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAA;QAElC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YACjB,IAAI,CAAC,YAAY,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAA;YAEvC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC1C,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,GAAG,UAAsB,CAAA;gBACjE,eAAe,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,2CAA2C,EAAE,eAAe,CAAC,CAAA;gBACvG,eAAe,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,gCAAgC,EAAE,eAAe,CAAC,CAAA;YAC9F,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,MAAM;QACJ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC9B,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;QACrC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,IAAI,EAAE,CAAA;QAEjE,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;QACpH,MAAM,YAAY,GAChB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;QAEnG,MAAM,eAAe,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAChG;;;;;;WAMG;QACH,MAAM,cAAc,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAG,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACnF,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC;YAChD,CAAC,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;YAC3B,CAAC,CAAC,cAAc,KAAK,IAAI,IAAI,cAAc,KAAK,SAAS,IAAI,cAAc,KAAK,EAAE,CAAA;QAEpF,MAAM,OAAO,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,CAAA;QAE3F,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,IAAI,SAAS,CAAA;YAEb,QAAQ,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;gBACjC,KAAK,GAAG;oBACN,SAAS,GAAG,MAAM,CAAA;oBAClB,MAAK;gBACP,KAAK,GAAG;oBACN,SAAS,GAAG,KAAK,CAAA;oBACjB,MAAK;gBACP,KAAK,GAAG;oBACN,SAAS,GAAG,QAAQ,CAAA;oBACpB,MAAK;YACT,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAA;QACP,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA,mBAAmB,SAAS,aAAa,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;QAClE,eAAe;YACf,CAAC,CAAC,YAAY;gBACZ,CAAC,CAAC,IAAI,CAAA;;0BAEU,QAAQ;wBACV,eAAe;wBACf,MAAM;uBACP,MAAM,CAAC,CAAA,eAAe,aAAf,eAAe,uBAAf,eAAe,CAAE,IAAI,KAAI,EAAE,CAAC;8BAC5B;gBACpB,CAAC,CAAC,IAAI,CAAA;gBACA,CAAA,cAAc,aAAd,cAAc,uBAAd,cAAc,CAAE,IAAI,EAAC,CAAC,CAAC,IAAI,CAAA,YAAY,cAAc,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE;gBAC3E,CAAA,cAAc,aAAd,cAAc,uBAAd,cAAc,CAAE,KAAK,KAAI,EAAE;mBACxB;YACX,CAAC,CAAC,IAAI,CAAA,EAAE;;;;YAIJ,WAAW,CAAC,GAAG,CACf,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAA;;4BAEL,QAAQ;0BACV,MAAM;0BACN,MAAM;yBACP,MAAM,CAAC,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,KAAI,EAAE,CAAC;wBAC3B,GAAG,IAAI,CAAC;wBACR,GAAG,IAAI,CAAC;;aAEnB,CACF;;UAED,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,CAAC,CAAC,IAAI,CAAA;;kBAEE,YAAY,CAAC,GAAG,CAChB,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAA;;kCAEL,QAAQ;gCACV,MAAM;gCACN,MAAM;+BACP,MAAM,CAAC,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,KAAI,EAAE,CAAC;;mBAEtC,CACF;;aAEJ;YACH,CAAC,CAAC,IAAI,CAAA,EAAE;;;;UAIR,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YACrB,OAAO,IAAI,CAAA;;0BAEK,QAAQ;wBACV,MAAM;wBACN,MAAM;uBACP,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;WAE/B,CAAA;QACH,CAAC,CAAC;;KAEL,CAAA;IACH,CAAC;IAED,eAAe;QACb,IAAI,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAA;QACrD,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAEnC,wCAAwC;QACxC,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,UAAU,CAAC,CAAA;YAC1E,KAAK,GAAG,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;QACjG,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,iBAAiB,CAClC,IAAI,CAAC,MAAM,CAAC,OAAO,EACnB,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,QAAQ,EACb,IAAI,EACJ;YACE,KAAK;SACN,EACD,GAAG,EAAE;YACH,OAAO,IAAI,CAAC,WAAW,CAAA;QACzB,CAAC,CACF,CAAA;IACH,CAAC;;AAvXM,iBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA+LF;CACF,AAjMY,CAiMZ;AAE2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;0CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wCAA4B;AAC3B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;0CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAAsB;AAKO;IAAvD,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;+CAA6B;AACpD;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;8CAAwB;AA7M5C,UAAU;IADtB,aAAa,CAAC,gBAAgB,CAAC;GACnB,UAAU,CAyXtB","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../record-view/record-view.js'\nimport './data-card-field.js'\nimport './data-card-gutter.js'\nimport './data-card-gutter-menu.js'\n\nimport { LitElement, PropertyValues, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { GristConfig, GristData, GristRecord } from '../types.js'\nimport { ZERO_CONFIG, ZERO_DATA, ZERO_RECORD } from '../configure/zero-config.js'\nimport { RecordViewHandler } from '../record-view/record-view-handler.js'\nimport { recordCardClickHandler } from './event-handlers/record-card-click-handler.js'\nimport { recordCardDblClickHandler } from './event-handlers/record-card-dblclick-handler.js'\n\n// TODO 로케일 설정에 따라서 포맷이 바뀌도록 해야한다.\nconst OPTIONS: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n hour12: false\n // timeZone: 'America/Los_Angeles'\n}\n\nfunction getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {\n try {\n const safeLocale = locale || 'en-US'\n return new Intl.DateTimeFormat(safeLocale, options)\n } catch (e) {\n return new Intl.DateTimeFormat('en-US', options)\n }\n}\n\nconst formatter = getSafeFormatter(navigator.language, OPTIONS)\n\n@customElement('ox-record-card')\nexport class RecordCard extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n position: relative;\n background-color: var(--data-card-record-card-background-color);\n /* Theme 정의가 누락된 application 에서도 합리적 radius 보장. */\n border-radius: var(--data-card-record-card-border-radius, var(--md-sys-shape-corner-medium));\n border: var(--data-card-record-card-border);\n height: min-content;\n padding: var(--spacing-small) var(--spacing-none);\n }\n\n :host(:hover) {\n background-color: var(--md-sys-color-surface);\n border: var(--data-card-record-card-border-hover);\n box-shadow: var(--data-card-record-card-boxshadow-hover);\n }\n\n :host([dirty])::before {\n content: '';\n position: absolute;\n left: var(--spacing-none);\n top: var(--spacing-none);\n\n width: var(--spacing-none);\n height: var(--spacing-none);\n border-top: var(--grid-record-dirty-border-top);\n border-right: var(--grid-record-dirty-border-left);\n }\n\n :host[emphasized-row] {\n background-color: var(--grid-record-emphasized-background-color);\n color: var(--grid-record-emphasized-color);\n }\n\n [dirty] {\n position: absolute;\n margin: var(--spacing-none);\n height: 20px;\n\n font: var(--grid-record-dirty-icon-font);\n text-indent: 1px;\n left: var(--spacing-none);\n top: var(--spacing-none);\n color: var(--grid-record-dirty-color, var(--md-sys-color-error));\n }\n\n [thumbnail] {\n /*\n 높이 대신 **비율**로 잡을 수 있다.\n\n 그림이 자산인 목록에서는 카드 폭이 달라져도 그림 칸의 비율이 같아야 나란히 읽힌다. 높이만\n 있으면 좁은 카드에서는 납작하고 넓은 카드에서는 홀쭉해진다. 둘 다 안 주면 지금까지처럼\n 내용 높이를 따른다.\n */\n height: var(--data-card-thumbnail-height, auto);\n aspect-ratio: var(--data-card-thumbnail-aspect, auto);\n /*\n 그림 안쪽 여백과 바탕도 밖에서 정한다.\n\n 목록마다 그림이 하는 일이 다르다. 사람 얼굴(직원 목록)은 여백이 있어야 카드 안에 놓인\n 것처럼 보이고, 자산 카탈로그의 그림은 **그림이 주인공**이라 여백이 그림을 작게 만든다.\n 바탕도 마찬가지다 — 배경이 없는 그림(투명 png · 3D 미리보기)은 놓일 면이 있어야 한다.\n */\n padding: var(--data-card-thumbnail-padding, var(--spacing-large));\n background-color: var(--data-card-thumbnail-background, transparent);\n box-sizing: border-box;\n /* 동심 안쪽 radius — Material 3 shape token (외곽 = corner-medium, 안쪽 =\n * corner-small). board-ui 카드와 동일 톤. */\n border-radius: var(--data-card-thumbnail-radius, var(--md-sys-shape-corner-small));\n overflow: hidden;\n }\n\n /*\n 그림이 없는 것도 **자리는 지킨다.**\n\n 전에는 값이 없으면 썸네일 요소를 아예 그리지 않았다. 그러면 한 목록 안에서 카드 높이가\n 들쭉날쭉해져 눈이 줄을 못 잡고, 「아직 그림이 없다」는 사실을 말할 자리도 사라진다.\n 빈 자리는 조용한 바탕으로 두고, 무엇이라고 부를지는 쓰는 쪽이 정한다.\n */\n [thumbnail][empty] {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--data-card-thumbnail-empty-background, var(--md-sys-color-surface-container));\n color: var(--data-card-thumbnail-empty-color, var(--md-sys-color-outline));\n font: var(--data-card-item-etc-font);\n }\n\n [thumbnail][empty] md-icon {\n --md-icon-size: 28px;\n }\n\n [content] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-tiny);\n margin: var(--data-card-item-margin);\n }\n\n [brief] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-tiny);\n overflow: hidden;\n }\n\n /*\n 곁들이는 칸들 — 이 자리에 규칙이 아예 없었다.\n\n details 는 「이름 : 값」이 여러 줄 서는 자리인데 그 줄들을 세우는 규칙이 없어서, 카드가\n 글자 크기에 따라 줄이 붙어 버리고 제목 묶음(brief)과도 구분되지 않았다. 세로로 세우고\n 간격을 준다 — 카드에서 「무엇인가」와 「어떤가」는 다른 묶음이다.\n\n 선으로 가르지는 않는다. 값이 없는 줄이 섞이는 목록(있을 때만 말하는 칸)에서는 선 아래에\n 아무것도 없는 카드가 생긴다.\n\n 「이름 : 값」이 아니라 짧은 표식 몇 개(상태 · 점수)를 다는 목록에서는 세로로 세우는 것이\n 자리만 먹는다. 그때는 화면이 한 줄로 뉘일 수 있다(--data-card-detail-direction: row).\n */\n [detail] {\n display: flex;\n flex-direction: var(--data-card-detail-direction, column);\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--spacing-tiny) var(--spacing-medium);\n margin-top: var(--spacing-small);\n overflow: hidden;\n }\n\n [gutters] {\n display: flex;\n flex-direction: row;\n gap: var(--spacing-small);\n margin: var(--spacing-small) var(--spacing-medium);\n justify-content: end;\n align-items: stretch;\n }\n\n /*\n ── 손이 갈 때만 나오는 단추 ────────────────────────────────────────\n 단추가 그 카드의 주된 할 일인 목록(결재함의 「상세정보」)에서는 늘 보여야 한다. 반대로\n 고르는 것이 주된 일인 목록(자산 카탈로그)에서 삭제 단추가 카드마다 떠 있으면, 사람이\n 고르려고 보는 자리에 되돌릴 수 없는 조작이 먼저 눈에 든다.\n\n 어느 쪽인지는 화면이 정한다(list.gutters: 'hover'). 눌리지 않게 함께 막아야 한다 —\n 보이지 않는 단추가 눌리는 것이 더 나쁘다.\n */\n /*\n 감춘 단추는 **자리도 차지하지 않는다.** opacity 만 0 으로 두면 카드마다 빈 줄이 남아,\n 늘 보이는 것보다 되레 카드가 길어진다. 그림 위 오른쪽 끝으로 띄운다 — 손이 갈 때만\n 나오는 단추가 있을 자리다. 그림이 어두울 수 있으니 옅은 바탕을 깐다.\n */\n :host([gutters-hover]) [gutters] {\n position: absolute;\n top: var(--spacing-small);\n right: var(--spacing-small);\n margin: var(--spacing-none);\n padding: 1px;\n border-radius: var(--md-sys-shape-corner-small);\n background-color: var(\n --data-card-gutters-floating-background,\n color-mix(in srgb, var(--md-sys-color-surface, #fff) 78%, transparent)\n );\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.12s ease-out;\n }\n\n :host([gutters-hover]:hover) [gutters],\n :host([gutters-hover]:focus-within) [gutters] {\n opacity: 1;\n pointer-events: auto;\n }\n\n ox-card-field {\n font: var(--data-card-item-etc-font);\n color: var(--data-card-item-etc-color, var(--md-sys-color-on-surface));\n }\n\n ox-card-field[name] {\n font: var(--data-card-item-name-font);\n color: var(--data-card-item-name-color, var(--md-sys-color-secondary));\n }\n\n ox-card-field[desc] {\n font: var(--data-card-item-disc-font);\n color: var(--data-card-item-disc-color, var(--md-sys-color-tertiary));\n }\n `\n ]\n\n @property({ type: Object }) config: GristConfig = ZERO_CONFIG\n @property({ type: Object }) data: GristData = ZERO_DATA\n @property({ type: Object }) record: GristRecord = ZERO_RECORD\n @property({ type: Number }) rowIndex: number = -1\n /*\n * row-selector를 사용자가 변경할 때, record-card의 update를 유도하기 위해 selected-row attribute를 property에 추가함.\n * (이를 해주지 않으면, 리스트 refresh 경우에 selected-row checkbox가 클리어되지 않는 현상이 발생함.)\n */\n @property({ type: Boolean, attribute: 'selected-row' }) selectedRow: boolean = false\n @property({ attribute: false }) emphasized: any = false\n\n private _recordView: any\n private clickHandler = recordCardClickHandler.bind(this) as EventListener\n private dblclickHandler = recordCardDblClickHandler.bind(this) as EventListener\n\n firstUpdated() {\n /*\n long-press\n TODO. performance를 확인한 후에 활성화하자.\n */\n // longpressable(this.renderRoot.querySelector('[content]'))\n // this.renderRoot.addEventListener('long-press', recordPartialLongPressHandler.bind(this))\n this.renderRoot.addEventListener('click', this.clickHandler)\n this.renderRoot.addEventListener('dblclick', this.dblclickHandler)\n\n this.addEventListener('show-record-view', () => this.popupRecordView())\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('record') && this._recordView) {\n this._recordView.record = this.record\n }\n\n if (changes.has('config')) {\n this.toggleAttribute('gutters-hover', this.config?.list?.gutters == 'hover')\n }\n\n const emphasized = this.emphasized\n\n if (!!emphasized) {\n this.setAttribute('emphasized-row', '')\n\n if (Symbol.iterator in Object(emphasized)) {\n const [backgroundColor, foregroundColor] = emphasized as string[]\n backgroundColor && this.style.setProperty('--grid-record-emphasized-background-color', backgroundColor)\n foregroundColor && this.style.setProperty('--grid-record-emphasized-color', foregroundColor)\n }\n } else {\n this.removeAttribute('emphasized-row')\n }\n }\n\n render() {\n const record = this.record\n const rowIndex = this.rowIndex\n const { columns, list } = this.config\n const { thumbnail, fields, details, emptyThumbnail } = list || {}\n\n const briefFields = fields.map(field => columns.find(column => column.name == field)).filter(column => column) || []\n const detailFields =\n details.map(field => columns.find(column => column.name == field)).filter(column => column) || []\n\n const thumbnailColumn = thumbnail ? columns.find(column => column.name == thumbnail) : undefined\n /*\n * 값이 없으면 썸네일 자리를 만들지 않는다.\n *\n * 전에는 설정에 썸네일 컬럼 이름이 있으면 **레코드에 값이 있는지 보지 않고** 그렸다. 그래서 이미지를\n * 갖지 않는 목록(결재함 · 업무함)에서 카드마다 깨진 이미지 표시가 세로의 절반을 차지하고, 정작\n * 제목은 잘렸다. 없는 것의 자리를 비워 두는 것이다.\n */\n const thumbnailValue = thumbnailColumn ? record?.[thumbnailColumn.name] : undefined\n const hasThumbnail = Array.isArray(thumbnailValue)\n ? thumbnailValue.length > 0\n : thumbnailValue !== null && thumbnailValue !== undefined && thumbnailValue !== ''\n\n const gutters = (columns || []).filter(column => column.type == 'gutter' && column.forList)\n\n if (this.hasAttribute('dirty')) {\n var dirtyIcon\n\n switch (this.record['__dirty__']) {\n case 'M':\n dirtyIcon = 'done'\n break\n case '+':\n dirtyIcon = 'add'\n break\n case '-':\n dirtyIcon = 'remove'\n break\n }\n }\n\n return html`\n ${dirtyIcon ? html` <md-icon dirty>${dirtyIcon}</md-icon> ` : html``}\n ${thumbnailColumn\n ? hasThumbnail\n ? html` <ox-card-field\n thumbnail\n .rowIndex=${rowIndex}\n .column=${thumbnailColumn}\n .record=${record}\n .value=${record[thumbnailColumn?.name || '']}\n ></ox-card-field>`\n : html`<div thumbnail empty>\n ${emptyThumbnail?.icon ? html`<md-icon>${emptyThumbnail.icon}</md-icon>` : ''}\n ${emptyThumbnail?.label || ''}\n </div>`\n : html``}\n\n <div content>\n <div brief>\n ${briefFields.map(\n (column, idx) => html`\n <ox-card-field\n .rowIndex=${rowIndex}\n .column=${column}\n .record=${record}\n .value=${record[column?.name || '']}\n ?name=${idx == 0}\n ?desc=${idx == 1}\n ></ox-card-field>\n `\n )}\n </div>\n ${detailFields.length > 0\n ? html`\n <div detail>\n ${detailFields.map(\n (column, idx) => html`\n <ox-card-field\n .rowIndex=${rowIndex}\n .column=${column}\n .record=${record}\n .value=${record[column?.name || '']}\n ></ox-card-field>\n `\n )}\n </div>\n `\n : html``}\n </div>\n\n <div gutters>\n ${gutters.map(gutter => {\n return html`\n <ox-card-gutter\n .rowIndex=${rowIndex}\n .column=${gutter}\n .record=${record}\n .value=${record[gutter.name]}\n ></ox-card-gutter>\n `\n })}\n </div>\n `\n }\n\n popupRecordView() {\n var titleField = this.config.list.fields[0] || 'name'\n var title = this.record[titleField]\n\n /* field가 오브젝트형인 경우에는 렌더러를 타이틀로 사용한다. */\n if (typeof title == 'object') {\n var column = this.config.columns.find(column => column.name == titleField)\n title = column?.record.renderer(title, column, this.record, this.rowIndex, this /* cautious */)\n }\n\n this._recordView = RecordViewHandler(\n this.config.columns,\n this.record,\n this.rowIndex,\n this,\n {\n title\n },\n () => {\n delete this._recordView\n }\n )\n }\n}\n"]}
@@ -520,13 +520,28 @@ export type GristEventHandlerSet = {
520
520
  *
521
521
  * @typedef {Object} ListConfig
522
522
  * @property {string} [thumbnail] - The name of the field to use as a thumbnail image for records in list mode.
523
+ * @property {object} [emptyThumbnail] - What to draw where a record has no thumbnail. The slot keeps its
524
+ * place either way so cards stay the same height; this says what it should say. Without it the slot is
525
+ * drawn empty.
523
526
  * @property {string[]} fields - An array of field names to display in the list mode.
524
527
  * @property {string[]} details - An array of field names to display as additional details in the list mode.
525
528
  */
526
529
  export type ListConfig = {
527
530
  thumbnail?: string;
531
+ emptyThumbnail?: {
532
+ /** Material symbol name. */
533
+ icon?: string;
534
+ /** One short line — already translated by the caller. */
535
+ label?: string;
536
+ };
528
537
  fields: string[];
529
538
  details: string[];
539
+ /**
540
+ * When gutter buttons show on a card. 'always' (default) keeps them visible — right where the
541
+ * button is the point of the card. 'hover' reveals them on hover or keyboard focus, for lists
542
+ * where picking is the point and a destructive button should not sit on every card.
543
+ */
544
+ gutters?: 'always' | 'hover';
530
545
  };
531
546
  /**
532
547
  * Configuration options for managing tree-like structures in a Grist table.