@operato/data-grist 10.19.0 → 10.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/src/configure/rows-option-builder.js +2 -1
  3. package/dist/src/configure/rows-option-builder.js.map +1 -1
  4. package/dist/src/data-card/data-card-field.d.ts +1 -0
  5. package/dist/src/data-card/data-card-field.js +9 -1
  6. package/dist/src/data-card/data-card-field.js.map +1 -1
  7. package/dist/src/data-card/data-card-gutter.js +30 -12
  8. package/dist/src/data-card/data-card-gutter.js.map +1 -1
  9. package/dist/src/data-card/data-card.d.ts +7 -0
  10. package/dist/src/data-card/data-card.js +237 -2
  11. package/dist/src/data-card/data-card.js.map +1 -1
  12. package/dist/src/data-card/record-card.js +33 -2
  13. package/dist/src/data-card/record-card.js.map +1 -1
  14. package/dist/src/data-grid/data-grid-field.js +1 -1
  15. package/dist/src/data-grid/data-grid-field.js.map +1 -1
  16. package/dist/src/filters/filter-styles.js +1 -1
  17. package/dist/src/filters/filter-styles.js.map +1 -1
  18. package/dist/src/gutters/gutter-sequence.d.ts +1 -0
  19. package/dist/src/personalizer/ox-grist-filter-personalizer.d.ts +1 -1
  20. package/dist/src/personalizer/ox-grist-filter-personalizer.js +11 -45
  21. package/dist/src/personalizer/ox-grist-filter-personalizer.js.map +1 -1
  22. package/dist/src/personalizer/ox-grist-personalizer.d.ts +1 -1
  23. package/dist/src/personalizer/ox-grist-personalizer.js +23 -86
  24. package/dist/src/personalizer/ox-grist-personalizer.js.map +1 -1
  25. package/dist/src/personalizer/personalizer-chrome.d.ts +5 -0
  26. package/dist/src/personalizer/personalizer-chrome.js +165 -0
  27. package/dist/src/personalizer/personalizer-chrome.js.map +1 -0
  28. package/dist/src/types.d.ts +13 -0
  29. package/dist/src/types.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
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.23.0](https://github.com/hatiolab/operato/compare/v10.22.0...v10.23.0) (2026-09-24)
7
+
8
+
9
+ ### :house: Code Refactoring
10
+
11
+ * read Material's colour names, not their --ui aliases ([390e4cf](https://github.com/hatiolab/operato/commit/390e4cf7bf35ea5223d0c82cde6941ad4f95fea0))
12
+
13
+
14
+ ### :bug: Bug Fix
15
+
16
+ * **attachment:** card actions as five quiet icons, so the row fits any card width ([1eb0665](https://github.com/hatiolab/operato/commit/1eb066581aa215957d98a1a8e7851af650c159ca))
17
+ * **attachment:** draw the tile for files with no server thumbnail; tooltips on card fields ([4a0ee28](https://github.com/hatiolab/operato/commit/4a0ee28396564f098aca8f0b365be65921d24e07))
18
+ * read custom properties that exist ([c9cec34](https://github.com/hatiolab/operato/commit/c9cec34d89485b058cfee8d91a06481b0cb9fa58))
19
+
20
+
21
+ ### :rocket: New Features
22
+
23
+ * **attachment:** quiet cards for the attachment list; outlined card gutters ([97081d1](https://github.com/hatiolab/operato/commit/97081d17e37d99ae231317938329ccb52b4bfae6))
24
+ * **attachment:** tiles for files that are not pictures; one-line card titles ([371cafc](https://github.com/hatiolab/operato/commit/371cafc2b9745a2b15b7870f843767aff5646632))
25
+ * **data-grist,property-panel:** card reorder, quiet gutters, panel heading ([3ad5843](https://github.com/hatiolab/operato/commit/3ad5843287c75007b52bbf18d6117fcffdbd1a83))
26
+ * **data-grist:** a proper personalizer popup for columns and filters ([4c3b620](https://github.com/hatiolab/operato/commit/4c3b620da53ce5dd80d820985d19d442c2d4ab0f))
27
+
28
+
29
+ ### :sparkles: Styling
30
+
31
+ * **data-grist:** smaller ticks in the personalizer rows ([6c2f048](https://github.com/hatiolab/operato/commit/6c2f04832d57e86b96e466799afd4d7d31792e16))
32
+
33
+
34
+
35
+ ## [10.22.0](https://github.com/hatiolab/operato/compare/v10.21.0...v10.22.0) (2026-09-24)
36
+
37
+ **Note:** Version bump only for package @operato/data-grist
38
+
39
+
40
+
41
+
42
+
6
43
  ## [10.19.0](https://github.com/hatiolab/operato/compare/v10.18.0...v10.19.0) (2026-09-18)
7
44
 
8
45
 
@@ -1,7 +1,7 @@
1
1
  import { NOOP } from '../const.js';
2
2
  import { getGristEventHandler } from '../handlers/index.js';
3
3
  export const buildRowsOptions = (rows) => {
4
- var { appendable = true, editable = true, insertable = false, selectable, groups = [], totals = [], classifier, accumulator } = rows;
4
+ var { appendable = true, editable = true, insertable = false, selectable, reorderable, groups = [], totals = [], classifier, accumulator } = rows;
5
5
  /* handler */
6
6
  var { click, dblclick, focus } = rows.handlers || {};
7
7
  if (typeof classifier !== 'function') {
@@ -13,6 +13,7 @@ export const buildRowsOptions = (rows) => {
13
13
  editable,
14
14
  insertable,
15
15
  selectable,
16
+ reorderable,
16
17
  groups,
17
18
  totals,
18
19
  handlers: {
@@ -1 +1 @@
1
- {"version":3,"file":"rows-option-builder.js","sourceRoot":"","sources":["../../../src/configure/rows-option-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAElC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAS,EAAc,EAAE;IACxD,IAAI,EACF,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,IAAI,EACf,UAAU,GAAG,KAAK,EAClB,UAAU,EACV,MAAM,GAAG,EAAE,EACX,MAAM,GAAG,EAAE,EACX,UAAU,EACV,WAAW,EACZ,GAAG,IAAI,CAAA;IAER,aAAa;IACb,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAA;IACpD,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,UAAU,GAAG,IAAI,CAAA;IACnB,CAAC;IAED,OAAO;QACL,WAAW;QACX,UAAU;QACV,QAAQ;QACR,UAAU;QACV,UAAU;QACV,MAAM;QACN,MAAM;QACN,QAAQ,EAAE;YACR,KAAK,EAAE,oBAAoB,CAAC,KAAK,CAAC;YAClC,QAAQ,EAAE,oBAAoB,CAAC,QAAQ,CAAC;YACxC,KAAK,EAAE,oBAAoB,CAAC,KAAK,CAAC;SACnC;QACD,UAAU;KACX,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { NOOP } from '../const.js'\nimport { RowsConfig } from '../types.js'\nimport { getGristEventHandler } from '../handlers/index.js'\n\nexport const buildRowsOptions = (rows: any): RowsConfig => {\n var {\n appendable = true,\n editable = true,\n insertable = false,\n selectable,\n groups = [],\n totals = [],\n classifier,\n accumulator\n } = rows\n\n /* handler */\n var { click, dblclick, focus } = rows.handlers || {}\n if (typeof classifier !== 'function') {\n classifier = NOOP\n }\n\n return {\n accumulator,\n appendable,\n editable,\n insertable,\n selectable,\n groups,\n totals,\n handlers: {\n click: getGristEventHandler(click),\n dblclick: getGristEventHandler(dblclick),\n focus: getGristEventHandler(focus)\n },\n classifier\n }\n}\n"]}
1
+ {"version":3,"file":"rows-option-builder.js","sourceRoot":"","sources":["../../../src/configure/rows-option-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAElC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAS,EAAc,EAAE;IACxD,IAAI,EACF,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,IAAI,EACf,UAAU,GAAG,KAAK,EAClB,UAAU,EACV,WAAW,EACX,MAAM,GAAG,EAAE,EACX,MAAM,GAAG,EAAE,EACX,UAAU,EACV,WAAW,EACZ,GAAG,IAAI,CAAA;IAER,aAAa;IACb,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAA;IACpD,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,UAAU,GAAG,IAAI,CAAA;IACnB,CAAC;IAED,OAAO;QACL,WAAW;QACX,UAAU;QACV,QAAQ;QACR,UAAU;QACV,UAAU;QACV,WAAW;QACX,MAAM;QACN,MAAM;QACN,QAAQ,EAAE;YACR,KAAK,EAAE,oBAAoB,CAAC,KAAK,CAAC;YAClC,QAAQ,EAAE,oBAAoB,CAAC,QAAQ,CAAC;YACxC,KAAK,EAAE,oBAAoB,CAAC,KAAK,CAAC;SACnC;QACD,UAAU;KACX,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { NOOP } from '../const.js'\nimport { RowsConfig } from '../types.js'\nimport { getGristEventHandler } from '../handlers/index.js'\n\nexport const buildRowsOptions = (rows: any): RowsConfig => {\n var {\n appendable = true,\n editable = true,\n insertable = false,\n selectable,\n reorderable,\n groups = [],\n totals = [],\n classifier,\n accumulator\n } = rows\n\n /* handler */\n var { click, dblclick, focus } = rows.handlers || {}\n if (typeof classifier !== 'function') {\n classifier = NOOP\n }\n\n return {\n accumulator,\n appendable,\n editable,\n insertable,\n selectable,\n reorderable,\n groups,\n totals,\n handlers: {\n click: getGristEventHandler(click),\n dblclick: getGristEventHandler(dblclick),\n focus: getGristEventHandler(focus)\n },\n classifier\n }\n}\n"]}
@@ -2,6 +2,7 @@ import { LitElement, TemplateResult } from 'lit';
2
2
  import { ColumnConfig, GristRecord } from '../types.js';
3
3
  export declare class DataCardField extends LitElement {
4
4
  static styles: import("lit").CSSResult[];
5
+ private tooltipController?;
5
6
  align?: string;
6
7
  record: GristRecord;
7
8
  column: ColumnConfig;
@@ -1,10 +1,17 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { css, html, LitElement } from 'lit';
3
3
  import { customElement, property } from 'lit/decorators.js';
4
+ import { TooltipReactiveController } from '@operato/utils';
4
5
  import { ZERO_COLUMN, ZERO_RECORD } from '../configure/zero-config.js';
5
6
  let DataCardField = class DataCardField extends LitElement {
6
7
  constructor() {
7
8
  super(...arguments);
9
+ /*
10
+ * Cut text gets a tooltip here as it does in a grid cell. The grid's cells and headers had the
11
+ * controller; the card's fields never did, so a card title clipped to its line count could not
12
+ * be read in full anywhere.
13
+ */
14
+ this.tooltipController = new TooltipReactiveController(this);
8
15
  this.record = ZERO_RECORD;
9
16
  this.column = ZERO_COLUMN;
10
17
  this.rowIndex = -1;
@@ -86,9 +93,10 @@ DataCardField.styles = [
86
93
  * 한 줄로 자르면 「[처분 승인] D-1788231051788-B · …」 처럼 **구별되는 부분이 잘려 나간다.**
87
94
  * 카드는 표와 달리 세로로 여유가 있고, 그 여유를 제목에 주는 것이 맞다.
88
95
  */
96
+ /* A list can ask for one line (--data-card-item-name-lines: 1) when its names all start alike. */
89
97
  :host([name]) > span {
90
98
  display: -webkit-box;
91
- -webkit-line-clamp: 2;
99
+ -webkit-line-clamp: var(--data-card-item-name-lines, 2);
92
100
  -webkit-box-orient: vertical;
93
101
  white-space: normal;
94
102
  overflow: hidden;
@@ -1 +1 @@
1
- {"version":3,"file":"data-card-field.js","sourceRoot":"","sources":["../../../src/data-card/data-card-field.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAkB,MAAM,KAAK,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAI/D,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QAmFuB,WAAM,GAAgB,WAAW,CAAA;QACjC,WAAM,GAAiB,WAAW,CAAA;QAClC,aAAQ,GAAW,CAAC,CAAC,CAAA;IAkBnD,CAAC;IAfC,MAAM;QACJ,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAE9C,IAAI,EACF,KAAK,EACL,MAAM,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,EACrC,GAAG,MAAM,CAAA;QAEV,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,KAAK,CAAA;YACvC,OAAO,IAAI,CAAA,UAAU,aAAa,CAAC,MAAM,CAAC,WAAW,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAA;QAC9G,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,CAAA,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAA;QACvE,CAAC;IACH,CAAC;;AArGM,oBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6EF;CACF,AA/EY,CA+EZ;AAE8B;IAA9B,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;4CAAe;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAmC;AAClC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;+CAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAAe;AAtF/B,aAAa;IADzB,aAAa,CAAC,eAAe,CAAC;GAClB,aAAa,CAuGzB","sourcesContent":["import { css, html, LitElement, 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-field')\nexport class DataCardField extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n position: relative;\n\n white-space: nowrap;\n\n font: inherit;\n }\n\n :host([thumbnail]) {\n flex-direction: column;\n justify-content: center;\n }\n\n :host([thumbnail]) > * {\n object-fit: contain;\n max-width: 100%;\n max-height: 100%;\n /* 그림 본체 모서리도 둥글게 — wrapper 의 radius 와 동일 (record-card 의\n * [thumbnail] wrapper 와 같은 변수 / fallback). */\n border-radius: var(--data-card-thumbnail-radius, var(--md-sys-shape-corner-small));\n }\n\n :host > * {\n margin: var(--spacing-none);\n\n overflow: hidden;\n\n text-overflow: ellipsis;\n text-align: left;\n }\n\n :host > *[center] {\n flex: none;\n margin: var(--spacing-none) auto;\n }\n\n :host([name]) label {\n display: none;\n }\n\n label {\n flex: none;\n width: 33%;\n padding-bottom: 2px;\n font: var(--data-card-item-etc-label-font);\n text-transform: capitalize;\n }\n\n :host([name]) > span,\n label + a {\n display: block;\n min-width: 100%;\n }\n\n /*\n * 카드의 제목은 두 줄까지 읽힌다.\n *\n * 한 줄로 자르면 「[처분 승인] D-1788231051788-B · …」 처럼 **구별되는 부분이 잘려 나간다.**\n * 카드는 표와 달리 세로로 여유가 있고, 그 여유를 제목에 주는 것이 맞다.\n */\n :host([name]) > span {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n white-space: normal;\n overflow: hidden;\n }\n\n @media only screen and (max-width: 460px) {\n *[center] {\n margin: initial;\n }\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 var { value, column, record, rowIndex } = this\n\n var {\n label,\n record: { renderer: recordRenderer }\n } = column\n\n if (typeof label == 'object') {\n let { renderer: labelRenderer } = label\n return html`<label>${labelRenderer(column)}</label>${recordRenderer(value, column, record, rowIndex, this)}`\n } else {\n return html`${recordRenderer(value, column, record, rowIndex, this)}`\n }\n }\n}\n"]}
1
+ {"version":3,"file":"data-card-field.js","sourceRoot":"","sources":["../../../src/data-card/data-card-field.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAkB,MAAM,KAAK,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAA;AAE1D,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAI/D,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QAmFL;;;;WAIG;QACK,sBAAiB,GAA+B,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAA;QAG/D,WAAM,GAAgB,WAAW,CAAA;QACjC,WAAM,GAAiB,WAAW,CAAA;QAClC,aAAQ,GAAW,CAAC,CAAC,CAAA;IAkBnD,CAAC;IAfC,MAAM;QACJ,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAE9C,IAAI,EACF,KAAK,EACL,MAAM,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,EACrC,GAAG,MAAM,CAAA;QAEV,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,KAAK,CAAA;YACvC,OAAO,IAAI,CAAA,UAAU,aAAa,CAAC,MAAM,CAAC,WAAW,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAA;QAC9G,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,CAAA,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAA;QACvE,CAAC;IACH,CAAC;;AA7GM,oBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA8EF;CACF,AAhFY,CAgFZ;AAS8B;IAA9B,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;4CAAe;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAkC;AACjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAmC;AAClC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;+CAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAAe;AA9F/B,aAAa;IADzB,aAAa,CAAC,eAAe,CAAC;GAClB,aAAa,CA+GzB","sourcesContent":["import { css, html, LitElement, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { TooltipReactiveController } from '@operato/utils'\n\nimport { ZERO_COLUMN, ZERO_RECORD } from '../configure/zero-config.js'\nimport { ColumnConfig, GristRecord } from '../types.js'\n\n@customElement('ox-card-field')\nexport class DataCardField extends LitElement {\n static styles = [\n css`\n :host {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n position: relative;\n\n white-space: nowrap;\n\n font: inherit;\n }\n\n :host([thumbnail]) {\n flex-direction: column;\n justify-content: center;\n }\n\n :host([thumbnail]) > * {\n object-fit: contain;\n max-width: 100%;\n max-height: 100%;\n /* 그림 본체 모서리도 둥글게 — wrapper 의 radius 와 동일 (record-card 의\n * [thumbnail] wrapper 와 같은 변수 / fallback). */\n border-radius: var(--data-card-thumbnail-radius, var(--md-sys-shape-corner-small));\n }\n\n :host > * {\n margin: var(--spacing-none);\n\n overflow: hidden;\n\n text-overflow: ellipsis;\n text-align: left;\n }\n\n :host > *[center] {\n flex: none;\n margin: var(--spacing-none) auto;\n }\n\n :host([name]) label {\n display: none;\n }\n\n label {\n flex: none;\n width: 33%;\n padding-bottom: 2px;\n font: var(--data-card-item-etc-label-font);\n text-transform: capitalize;\n }\n\n :host([name]) > span,\n label + a {\n display: block;\n min-width: 100%;\n }\n\n /*\n * 카드의 제목은 두 줄까지 읽힌다.\n *\n * 한 줄로 자르면 「[처분 승인] D-1788231051788-B · …」 처럼 **구별되는 부분이 잘려 나간다.**\n * 카드는 표와 달리 세로로 여유가 있고, 그 여유를 제목에 주는 것이 맞다.\n */\n /* A list can ask for one line (--data-card-item-name-lines: 1) when its names all start alike. */\n :host([name]) > span {\n display: -webkit-box;\n -webkit-line-clamp: var(--data-card-item-name-lines, 2);\n -webkit-box-orient: vertical;\n white-space: normal;\n overflow: hidden;\n }\n\n @media only screen and (max-width: 460px) {\n *[center] {\n margin: initial;\n }\n }\n `\n ]\n\n /*\n * Cut text gets a tooltip here as it does in a grid cell. The grid's cells and headers had the\n * controller; the card's fields never did, so a card title clipped to its line count could not\n * be read in full anywhere.\n */\n private tooltipController?: TooltipReactiveController = new TooltipReactiveController(this)\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 var { value, column, record, rowIndex } = this\n\n var {\n label,\n record: { renderer: recordRenderer }\n } = column\n\n if (typeof label == 'object') {\n let { renderer: labelRenderer } = label\n return html`<label>${labelRenderer(column)}</label>${recordRenderer(value, column, record, rowIndex, this)}`\n } else {\n return html`${recordRenderer(value, column, record, rowIndex, this)}`\n }\n }\n}\n"]}
@@ -34,6 +34,12 @@ let DataCardGutter = class DataCardGutter extends LitElement {
34
34
  this.removeAttribute('danger');
35
35
  }
36
36
  }
37
+ /* `quiet` may depend on the record (a set favorite stays visible), so it follows both. */
38
+ if (changes.has('column') || changes.has('record')) {
39
+ const { quiet } = this.column;
40
+ const isQuiet = typeof quiet === 'function' ? !!quiet(this.record) : !!quiet;
41
+ this.toggleAttribute('quiet', isQuiet);
42
+ }
37
43
  }
38
44
  };
39
45
  DataCardGutter.styles = [
@@ -50,14 +56,20 @@ DataCardGutter.styles = [
50
56
  margin-right: auto;
51
57
  }
52
58
 
59
+ /*
60
+ * A worded gutter is an outlined button in the card's own ink - it is one of several equal
61
+ * actions on the card, not the card's primary action, so it takes no fill. The destructive
62
+ * one is filled, in the theme's danger pair (the scheme's error tokens unless a theme hands
63
+ * in its own, as a dark theme does to avoid a pastel plate).
64
+ */
53
65
  * {
54
- border: 0;
66
+ border: 1px solid var(--md-sys-color-outline-variant);
55
67
  font-size: var(--md-sys-typescale-label-medium-size, 0.8rem);
56
68
  line-height: var(--md-sys-typescale-label-medium-size, 0.8rem);
57
69
 
58
70
  color: var(--md-sys-color-on-primary);
59
71
  background-color: var(--md-sys-color-primary);
60
- border-radius: var(--data-card-item-btn-border-radius, var(--md-sys-shape-corner-tiny));
72
+ border-radius: var(--data-card-item-btn-border-radius, var(--md-sys-shape-corner-extra-small, 4px));
61
73
 
62
74
  padding: var(--spacing-small);
63
75
  }
@@ -74,8 +86,9 @@ DataCardGutter.styles = [
74
86
  --md-icon-size: var(--data-card-gutter-icon-size, 20px);
75
87
  font-size: var(--data-card-gutter-icon-size, 20px);
76
88
  line-height: 1;
89
+ border: 0;
77
90
  padding: var(--spacing-tiny);
78
- color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
91
+ color: var(--data-card-gutter-icon-color, var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface)));
79
92
  background-color: transparent;
80
93
  border-radius: var(--md-sys-shape-corner-small);
81
94
  cursor: pointer;
@@ -97,27 +110,29 @@ DataCardGutter.styles = [
97
110
  * 그려서 부자연. 자식들은 button hover 의 효과를 상속받기만 한다. */
98
111
  button:hover {
99
112
  cursor: pointer;
100
- color: var(--md-sys-color-on-secondary);
101
- background-color: var(--md-sys-color-secondary);
113
+ color: var(--md-sys-color-on-secondary-container);
114
+ background-color: var(--md-sys-color-secondary-container);
115
+ border-color: transparent;
102
116
  }
103
117
 
104
118
  :host([danger]) button {
105
- color: var(--md-sys-color-on-error);
106
- background-color: var(--md-sys-color-error);
107
- border-color: var(--md-sys-color-error);
119
+ color: var(--danger-button-color, var(--md-sys-color-on-error));
120
+ background-color: var(--danger-button-background, var(--md-sys-color-error));
121
+ border-color: transparent;
108
122
  }
109
123
 
110
124
  :host([danger]) button:hover {
111
- color: var(--md-sys-color-error);
112
- background-color: var(--md-sys-color-on-secondary);
113
- border-color: var(--md-sys-color-on-secondary);
125
+ color: var(--danger-button-hover-color, var(--md-sys-color-error));
126
+ background-color: var(--danger-button-hover-background, var(--md-sys-color-on-error));
127
+ border-color: transparent;
114
128
  }
115
129
 
116
130
  button {
117
131
  display: flex;
118
132
  gap: var(--spacing-small);
119
- border: 0;
120
133
  align-items: center;
134
+ font: inherit;
135
+ font-size: var(--md-sys-typescale-label-medium-size, 0.8rem);
121
136
  }
122
137
 
123
138
  /* 자식 (span / md-icon) 은 button 의 color 를 inherit — button hover / danger
@@ -128,12 +143,15 @@ DataCardGutter.styles = [
128
143
  }
129
144
 
130
145
  button md-icon {
146
+ --md-icon-size: 16px;
147
+ font-size: 16px;
131
148
  border: 0;
132
149
  padding: 0;
133
150
  background-color: unset;
134
151
  }
135
152
 
136
153
  button span {
154
+ border: 0;
137
155
  padding: 0;
138
156
  background-color: transparent;
139
157
  }
@@ -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;;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"]}
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;;QAsHuB,WAAM,GAAgB,WAAW,CAAA;QACjC,WAAM,GAAiB,WAAW,CAAA;QAClC,aAAQ,GAAW,CAAC,CAAC,CAAA;IAwCnD,CAAC;IArCC,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;QAED,0FAA0F;QAC1F,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnD,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;YAC7B,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;YAE5E,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;;AA9JM,qBAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgHF;CACF,AAlHY,CAkHZ;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;AAzH/B,cAAc;IAD1B,aAAa,CAAC,gBAAgB,CAAC;GACnB,cAAc,CAgK1B","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 * A worded gutter is an outlined button in the card's own ink - it is one of several equal\n * actions on the card, not the card's primary action, so it takes no fill. The destructive\n * one is filled, in the theme's danger pair (the scheme's error tokens unless a theme hands\n * in its own, as a dark theme does to avoid a pastel plate).\n */\n * {\n border: 1px solid var(--md-sys-color-outline-variant);\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-extra-small, 4px));\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 border: 0;\n padding: var(--spacing-tiny);\n color: var(--data-card-gutter-icon-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-container);\n background-color: var(--md-sys-color-secondary-container);\n border-color: transparent;\n }\n\n :host([danger]) button {\n color: var(--danger-button-color, var(--md-sys-color-on-error));\n background-color: var(--danger-button-background, var(--md-sys-color-error));\n border-color: transparent;\n }\n\n :host([danger]) button:hover {\n color: var(--danger-button-hover-color, var(--md-sys-color-error));\n background-color: var(--danger-button-hover-background, var(--md-sys-color-on-error));\n border-color: transparent;\n }\n\n button {\n display: flex;\n gap: var(--spacing-small);\n align-items: center;\n font: inherit;\n font-size: var(--md-sys-typescale-label-medium-size, 0.8rem);\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 --md-icon-size: 16px;\n font-size: 16px;\n border: 0;\n padding: 0;\n background-color: unset;\n }\n\n button span {\n border: 0;\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 /* `quiet` may depend on the record (a set favorite stays visible), so it follows both. */\n if (changes.has('column') || changes.has('record')) {\n const { quiet } = this.column\n const isQuiet = typeof quiet === 'function' ? !!quiet(this.record) : !!quiet\n\n this.toggleAttribute('quiet', isQuiet)\n }\n }\n}\n"]}
@@ -15,7 +15,14 @@ export declare class DataCard extends DataManipulator {
15
15
  private _total;
16
16
  private _limit;
17
17
  private _page;
18
+ private _draggedId;
19
+ private _dropTargetId;
20
+ private _dropSide;
18
21
  firstUpdated(changes: PropertyValues<this>): void;
22
+ private _wireReorder;
23
+ private _onDrop;
24
+ private _clearDropIndicator;
25
+ private _resetDragState;
19
26
  updated(changes: PropertyValues<this>): void;
20
27
  render(): import("lit-html").TemplateResult<1>;
21
28
  gotoTop(e: Event): void;
@@ -3,7 +3,7 @@ import '@material/web/icon/icon.js';
3
3
  import '@material/web/button/text-button.js';
4
4
  import '@operato/list-state/ox-empty-note.js';
5
5
  import './record-card.js';
6
- import { css, html } from 'lit';
6
+ import { css, html, nothing } from 'lit';
7
7
  import { customElement, property, state } from 'lit/decorators.js';
8
8
  import { DataManipulator } from '../data-manipulator.js';
9
9
  import { renderEmptyNote } from '../empty-note-render.js';
@@ -15,6 +15,10 @@ let DataCard = class DataCard extends DataManipulator {
15
15
  this._total = 0;
16
16
  this._limit = 20;
17
17
  this._page = 1;
18
+ /* Drag-reorder state (`rows.reorderable`) - board-tile-list.ts's proven shape, generalized. */
19
+ this._draggedId = null;
20
+ this._dropTargetId = null;
21
+ this._dropSide = null;
18
22
  }
19
23
  firstUpdated(changes) {
20
24
  this.isTop = true;
@@ -32,6 +36,175 @@ let DataCard = class DataCard extends DataManipulator {
32
36
  }
33
37
  this.isTop = this.scrollTop == 0;
34
38
  });
39
+ this._wireReorder();
40
+ }
41
+ /*
42
+ * `config.rows.reorderable` is read inside each handler, not at listener-registration time - the
43
+ * listeners are cheap to hold and this sidesteps any question of whether `config` has already been
44
+ * assigned by the time `firstUpdated` runs (it is a plain property set by the caller's template,
45
+ * not guaranteed ordered against connection).
46
+ */
47
+ _wireReorder() {
48
+ this.renderRoot.addEventListener('dragstart', (e) => {
49
+ var _a, _b, _c;
50
+ if (!((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.rows) === null || _b === void 0 ? void 0 : _b.reorderable))
51
+ return;
52
+ const target = e.target.closest('[card][id]');
53
+ if (!(target === null || target === void 0 ? void 0 : target.id))
54
+ return;
55
+ this._draggedId = target.id;
56
+ target.classList.add('dragging');
57
+ e.dataTransfer.effectAllowed = 'move';
58
+ (_c = e.dataTransfer) === null || _c === void 0 ? void 0 : _c.setData('text/plain', target.id);
59
+ });
60
+ this.renderRoot.addEventListener('dragover', (e) => {
61
+ var _a, _b;
62
+ if (!((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.rows) === null || _b === void 0 ? void 0 : _b.reorderable) || !this._draggedId)
63
+ return;
64
+ e.preventDefault();
65
+ e.dataTransfer.dropEffect = 'move';
66
+ const target = e.target.closest('[card][id]');
67
+ if (!(target === null || target === void 0 ? void 0 : target.id) || target.id === this._draggedId)
68
+ return;
69
+ const targetRect = target.getBoundingClientRect();
70
+ const mousePos = e.clientX - targetRect.left;
71
+ const side = mousePos < targetRect.width / 2 ? 'before' : 'after';
72
+ if (this._dropTargetId !== target.id || this._dropSide !== side) {
73
+ this._clearDropIndicator();
74
+ this._dropTargetId = target.id;
75
+ this._dropSide = side;
76
+ target.classList.add(side === 'before' ? 'drop-before' : 'drop-after');
77
+ }
78
+ });
79
+ this.renderRoot.addEventListener('dragleave', (e) => {
80
+ var _a, _b;
81
+ if (!((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.rows) === null || _b === void 0 ? void 0 : _b.reorderable))
82
+ return;
83
+ const target = e.target.closest('[card][id]');
84
+ if ((target === null || target === void 0 ? void 0 : target.id) === this._dropTargetId) {
85
+ const related = e.relatedTarget;
86
+ if (!related || !target.contains(related)) {
87
+ this._clearDropIndicator();
88
+ }
89
+ }
90
+ });
91
+ this.renderRoot.addEventListener('drop', (e) => this._onDrop(e));
92
+ this.renderRoot.addEventListener('dragend', (e) => {
93
+ var _a, _b;
94
+ if (!((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.rows) === null || _b === void 0 ? void 0 : _b.reorderable))
95
+ return;
96
+ this._clearDropIndicator();
97
+ const target = e.target.closest('[card]');
98
+ if (target)
99
+ target.classList.remove('dragging');
100
+ this._resetDragState();
101
+ });
102
+ }
103
+ _onDrop(e) {
104
+ var _a, _b;
105
+ if (!((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.rows) === null || _b === void 0 ? void 0 : _b.reorderable))
106
+ return;
107
+ e.preventDefault();
108
+ /* drop 이 먼저 계산한다 - dragleave 가 먼저 일어나 상태를 지울 수 있어서다. */
109
+ const dropTarget = e.target.closest('[card][id]');
110
+ let dropTargetId = this._dropTargetId;
111
+ let dropSide = this._dropSide;
112
+ if ((dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) && dropTarget.id !== this._draggedId) {
113
+ dropTargetId = dropTarget.id;
114
+ const rect = dropTarget.getBoundingClientRect();
115
+ dropSide = e.clientX - rect.left < rect.width / 2 ? 'before' : 'after';
116
+ }
117
+ this._clearDropIndicator();
118
+ const draggedEl = this._draggedId ? this.renderRoot.querySelector(`[card][id="${this._draggedId}"]`) : null;
119
+ draggedEl === null || draggedEl === void 0 ? void 0 : draggedEl.classList.remove('dragging');
120
+ if (!this._draggedId || !dropTargetId || this._draggedId === dropTargetId) {
121
+ this._resetDragState();
122
+ return;
123
+ }
124
+ const records = [...this._records];
125
+ const draggedIndex = records.findIndex(r => r.id === this._draggedId);
126
+ if (draggedIndex === -1) {
127
+ this._resetDragState();
128
+ return;
129
+ }
130
+ const draggedId = this._draggedId;
131
+ const [dragged] = records.splice(draggedIndex, 1);
132
+ let targetIndex = records.findIndex(r => r.id === dropTargetId);
133
+ if (dropSide === 'after')
134
+ targetIndex++;
135
+ records.splice(targetIndex, 0, dragged);
136
+ this._resetDragState();
137
+ /* FLIP: 옮기기 전 자리를 먼저 잰다. */
138
+ const cards = this.renderRoot.querySelectorAll('[card][id]');
139
+ const firstPositions = new Map();
140
+ cards.forEach(card => firstPositions.set(card.id, card.getBoundingClientRect()));
141
+ /* 낙관적 갱신 - 지속은 이 이벤트를 듣는 쪽의 몫이다(RowsConfig.reorderable 문서 참조). */
142
+ this._records = records;
143
+ this.updateComplete.then(() => {
144
+ const updatedCards = this.renderRoot.querySelectorAll('[card][id]');
145
+ let droppedEl = null;
146
+ updatedCards.forEach(card => {
147
+ const first = firstPositions.get(card.id);
148
+ if (!first)
149
+ return;
150
+ const last = card.getBoundingClientRect();
151
+ const dx = first.left - last.left;
152
+ const dy = first.top - last.top;
153
+ const el = card;
154
+ if (card.id === draggedId) {
155
+ /* 옮긴 카드는 다른 카드들이 밀리는 동안 숨겨 뒀다가 마지막에 착지시킨다. */
156
+ el.classList.add('flip-dropped-hidden');
157
+ droppedEl = el;
158
+ }
159
+ else {
160
+ if (dx === 0 && dy === 0)
161
+ return;
162
+ el.style.transform = `translate(${dx}px, ${dy}px)`;
163
+ el.style.transition = 'none';
164
+ requestAnimationFrame(() => {
165
+ el.classList.add('flip-moving');
166
+ el.style.transform = '';
167
+ el.style.transition = '';
168
+ el.addEventListener('transitionend', () => el.classList.remove('flip-moving'), { once: true });
169
+ });
170
+ }
171
+ });
172
+ const ids = records.map(r => r.id);
173
+ const dispatchReorder = () => {
174
+ this.dispatchEvent(new CustomEvent('reorder', {
175
+ bubbles: true,
176
+ composed: true,
177
+ detail: { ids, movedId: draggedId }
178
+ }));
179
+ };
180
+ if (droppedEl) {
181
+ setTimeout(() => {
182
+ const el = droppedEl;
183
+ el.classList.remove('flip-dropped-hidden');
184
+ el.classList.add('flip-dropped');
185
+ el.addEventListener('animationend', () => {
186
+ el.classList.remove('flip-dropped');
187
+ dispatchReorder();
188
+ }, { once: true });
189
+ }, 500);
190
+ }
191
+ else {
192
+ dispatchReorder();
193
+ }
194
+ });
195
+ }
196
+ _clearDropIndicator() {
197
+ var _a;
198
+ if (this._dropTargetId) {
199
+ (_a = this.renderRoot.querySelector(`[card][id="${this._dropTargetId}"]`)) === null || _a === void 0 ? void 0 : _a.classList.remove('drop-before', 'drop-after');
200
+ }
201
+ this._dropTargetId = null;
202
+ this._dropSide = null;
203
+ }
204
+ _resetDragState() {
205
+ this._draggedId = null;
206
+ this._dropTargetId = null;
207
+ this._dropSide = null;
35
208
  }
36
209
  updated(changes) {
37
210
  if (changes.has('config')) {
@@ -46,7 +219,7 @@ let DataCard = class DataCard extends DataManipulator {
46
219
  }
47
220
  }
48
221
  render() {
49
- var { classifier } = this.config.rows;
222
+ var { classifier, reorderable } = this.config.rows;
50
223
  var records = this._records || [];
51
224
  if (this.config && this.config.rows.appendable) {
52
225
  records = [...records, { __dirty__: '+' }];
@@ -54,6 +227,8 @@ let DataCard = class DataCard extends DataManipulator {
54
227
  return html `
55
228
  ${records.map((record, rowIndex) => {
56
229
  var { emphasized } = classifier.call(null, record, rowIndex) || {};
230
+ /* Only a real record can be dragged - the trailing `appendable` placeholder has no id. */
231
+ var draggable = reorderable && record.id;
57
232
  return html `
58
233
  <ox-record-card
59
234
  .config=${this.config}
@@ -63,6 +238,8 @@ let DataCard = class DataCard extends DataManipulator {
63
238
  .emphasized=${emphasized}
64
239
  ?selected-row=${record['__selected__']}
65
240
  ?dirty=${record['__dirty__']}
241
+ id=${record.id || nothing}
242
+ draggable=${draggable ? 'true' : 'false'}
66
243
  card
67
244
  ></ox-record-card>
68
245
  `;
@@ -94,7 +271,17 @@ DataCard.styles = [
94
271
  기본값은 지금까지의 값 그대로다.
95
272
  */
96
273
  grid-template-columns: repeat(auto-fill, minmax(var(--data-card-min-width, 300px), 1fr));
274
+ /*
275
+ grid-template-rows: min-content only ever sizes the *first* row track - every row after
276
+ it is implicit, and an implicit track with no grid-auto-rows falls back to the browser's
277
+ default auto sizing, which is a looser (max-content-leaning) measurement than
278
+ min-content. With enough records to wrap past one row, that mismatch showed up as a real
279
+ gap: row 1 sized tight at ~201px, rows 2+ sized to ~319px while the cards inside still only
280
+ filled ~201px of it (operato-board's board-list, 2026-09-23 - 17 boards, 7 per row).
281
+ grid-auto-rows repeats the same rule for every implicit row, so all rows size alike.
282
+ */
97
283
  grid-template-rows: min-content;
284
+ grid-auto-rows: min-content;
98
285
  grid-gap: var(--spacing-large);
99
286
  }
100
287
 
@@ -157,6 +344,54 @@ DataCard.styles = [
157
344
  color: var(--grid-record-emphasized-color);
158
345
  }
159
346
 
347
+ /*
348
+ 드래그 순서 바꾸기 (rows.reorderable) — board-list 의 board-tile-list.ts 에서 검증된 모양을
349
+ 그대로 가져온다. 자리(placement)만 이 파일이 정하고, 지속(persist)은 소비자가 한다 — 그
350
+ 경계는 위 types.ts RowsConfig.reorderable 문서에 있다.
351
+ */
352
+ [card][draggable='true'] {
353
+ cursor: grab;
354
+ transition:
355
+ opacity 0.2s ease,
356
+ transform 0.2s ease;
357
+ }
358
+
359
+ [card].dragging {
360
+ opacity: 0.3;
361
+ transform: scale(0.95);
362
+ }
363
+
364
+ [card].drop-before {
365
+ border-left: 3px solid var(--md-sys-color-primary);
366
+ }
367
+
368
+ [card].drop-after {
369
+ border-right: 3px solid var(--md-sys-color-primary);
370
+ }
371
+
372
+ [card].flip-moving {
373
+ transition: transform 1s cubic-bezier(0.25, 0.1, 0.25, 1);
374
+ }
375
+
376
+ @keyframes ox-card-drop-land {
377
+ 0% {
378
+ opacity: 0;
379
+ transform: scale(0.9);
380
+ }
381
+ 100% {
382
+ opacity: 1;
383
+ transform: scale(1);
384
+ }
385
+ }
386
+
387
+ [card].flip-dropped-hidden {
388
+ opacity: 0;
389
+ }
390
+
391
+ [card].flip-dropped {
392
+ animation: ox-card-drop-land 0.5s cubic-bezier(0.22, 0.6, 0.36, 1);
393
+ }
394
+
160
395
  @media screen and (max-width: 460px) {
161
396
  #setting {
162
397
  grid-template-columns: 1fr;