@operato/utils 10.7.0 → 10.15.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 +19 -0
  2. package/coverage/lcov-report/base.css +224 -0
  3. package/coverage/lcov-report/block-navigation.js +87 -0
  4. package/coverage/lcov-report/favicon.png +0 -0
  5. package/coverage/lcov-report/index.html +131 -0
  6. package/coverage/lcov-report/prettify.css +1 -0
  7. package/coverage/lcov-report/prettify.js +2 -0
  8. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  9. package/coverage/lcov-report/sorter.js +210 -0
  10. package/coverage/lcov.info +429 -0
  11. package/dist/src/has-overflow.js +23 -1
  12. package/dist/src/has-overflow.js.map +1 -1
  13. package/dist/src/list-state.d.ts +47 -0
  14. package/dist/src/list-state.js +50 -0
  15. package/dist/src/list-state.js.map +1 -0
  16. package/dist/src/reactive-controllers/tooltip-reactive-controller.js +16 -3
  17. package/dist/src/reactive-controllers/tooltip-reactive-controller.js.map +1 -1
  18. package/dist/test/has-overflow.test.d.ts +1 -0
  19. package/dist/test/has-overflow.test.js +49 -0
  20. package/dist/test/has-overflow.test.js.map +1 -0
  21. package/dist/test/list-state.test.d.ts +1 -0
  22. package/dist/test/list-state.test.js +53 -0
  23. package/dist/test/list-state.test.js.map +1 -0
  24. package/dist/test/tooltip-reactive-controller.test.d.ts +1 -0
  25. package/dist/test/tooltip-reactive-controller.test.js +78 -0
  26. package/dist/test/tooltip-reactive-controller.test.js.map +1 -0
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +7 -3
  29. package/test/has-overflow.test.ts +61 -0
  30. package/test/list-state.test.ts +62 -0
  31. package/test/tooltip-reactive-controller.test.ts +88 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What a list has to say about itself - pure, so a screen and a grid can share one rule.
3
+ *
4
+ * Lists used to draw "whatever arrived". But the server cut at `limit` and the client cut again with
5
+ * `slice()`, and neither left a mark on the screen (operato-twin, 2026-07-31: ledger limit 120 + slice
6
+ * 120, entity 360 slice 40). A cut list cannot be told apart from "there is no more": people believe
7
+ * what is missing does not exist. A failed load looked the same as an empty one.
8
+ *
9
+ * So the rule lives here, once: loading, failed, empty, empty-because-narrowed and ready never mix,
10
+ * and a cut list says it was cut.
11
+ *
12
+ * Moved from operato-twin (client/list/list-model.ts), the most capable of the three list-state forks.
13
+ */
14
+ export function listPhase(scale, opts = {}) {
15
+ if (opts.error)
16
+ return 'error';
17
+ if (opts.loading && scale.loaded === 0)
18
+ return 'loading';
19
+ if (scale.shown === 0)
20
+ return scale.filtered ? 'empty-filtered' : 'empty';
21
+ return 'ready';
22
+ }
23
+ /** Is there more to fetch - when the total is unknown, a capped list says yes (unknown is not "none"). */
24
+ export function hasMore(scale) {
25
+ if (scale.total !== undefined)
26
+ return scale.loaded < scale.total;
27
+ return !!scale.capped;
28
+ }
29
+ /**
30
+ * The count, as an i18n key and its parameters - the sentence is not built here.
31
+ *
32
+ * Four cases, and telling them apart is the point:
33
+ * everything is here, nothing narrowed the count
34
+ * narrowed, part of what is held "shown of loaded"
35
+ * the server has more (total known) "shown of total"
36
+ * the server's cap was hit (total unknown) "first N - narrow to see the rest"
37
+ */
38
+ export function listCountLabel(scale) {
39
+ if (scale.capped && scale.total === undefined) {
40
+ return { key: 'text.list-count-capped', params: { loaded: scale.loaded } };
41
+ }
42
+ if (scale.total !== undefined && scale.total > scale.loaded) {
43
+ return { key: 'text.list-count-of-total', params: { shown: scale.shown, total: scale.total } };
44
+ }
45
+ if (scale.filtered && scale.shown !== scale.loaded) {
46
+ return { key: 'text.list-count-of-loaded', params: { shown: scale.shown, loaded: scale.loaded } };
47
+ }
48
+ return { key: 'text.list-count', params: { shown: scale.shown } };
49
+ }
50
+ //# sourceMappingURL=list-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-state.js","sourceRoot":"","sources":["../../src/list-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH,MAAM,UAAU,SAAS,CAAC,KAAgB,EAAE,OAA+C,EAAE;IAC3F,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAA;IAC9B,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACxD,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAA;IACzE,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,OAAO,CAAC,KAAgB;IACtC,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAA;IAChE,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;AACvB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,EAAE,GAAG,EAAE,wBAAwB,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;IAC5E,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5D,OAAO,EAAE,GAAG,EAAE,0BAA0B,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;IAChG,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACnD,OAAO,EAAE,GAAG,EAAE,2BAA2B,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;IACnG,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;AACnE,CAAC","sourcesContent":["/**\n * What a list has to say about itself - pure, so a screen and a grid can share one rule.\n *\n * Lists used to draw \"whatever arrived\". But the server cut at `limit` and the client cut again with\n * `slice()`, and neither left a mark on the screen (operato-twin, 2026-07-31: ledger limit 120 + slice\n * 120, entity 360 slice 40). A cut list cannot be told apart from \"there is no more\": people believe\n * what is missing does not exist. A failed load looked the same as an empty one.\n *\n * So the rule lives here, once: loading, failed, empty, empty-because-narrowed and ready never mix,\n * and a cut list says it was cut.\n *\n * Moved from operato-twin (client/list/list-model.ts), the most capable of the three list-state forks.\n */\n\n/** A list's size - the input that decides what a screen has to say. */\nexport interface ListScale {\n /** Rows drawn now, after narrowing. */\n shown: number\n /** Rows the client holds, before narrowing. */\n loaded: number\n /** Everything the server knows of. Undefined when unknown - never replaced by 0 (missing is not zero). */\n total?: number\n /** The server hit its own cap. Then `loaded` is a lower bound, not the whole. */\n capped?: boolean\n /** A search or filter is applied - it tells the two kinds of empty apart. */\n filtered?: boolean\n}\n\n/** What the screen shows. These do not mix. */\nexport type ListPhase = 'loading' | 'error' | 'empty' | 'empty-filtered' | 'ready'\n\nexport function listPhase(scale: ListScale, opts: { loading?: boolean; error?: unknown } = {}): ListPhase {\n if (opts.error) return 'error'\n if (opts.loading && scale.loaded === 0) return 'loading'\n if (scale.shown === 0) return scale.filtered ? 'empty-filtered' : 'empty'\n return 'ready'\n}\n\n/** Is there more to fetch - when the total is unknown, a capped list says yes (unknown is not \"none\"). */\nexport function hasMore(scale: ListScale): boolean {\n if (scale.total !== undefined) return scale.loaded < scale.total\n return !!scale.capped\n}\n\n/**\n * The count, as an i18n key and its parameters - the sentence is not built here.\n *\n * Four cases, and telling them apart is the point:\n * everything is here, nothing narrowed the count\n * narrowed, part of what is held \"shown of loaded\"\n * the server has more (total known) \"shown of total\"\n * the server's cap was hit (total unknown) \"first N - narrow to see the rest\"\n */\nexport function listCountLabel(scale: ListScale): { key: string; params: Record<string, number> } {\n if (scale.capped && scale.total === undefined) {\n return { key: 'text.list-count-capped', params: { loaded: scale.loaded } }\n }\n if (scale.total !== undefined && scale.total > scale.loaded) {\n return { key: 'text.list-count-of-total', params: { shown: scale.shown, total: scale.total } }\n }\n if (scale.filtered && scale.shown !== scale.loaded) {\n return { key: 'text.list-count-of-loaded', params: { shown: scale.shown, loaded: scale.loaded } }\n }\n return { key: 'text.list-count', params: { shown: scale.shown } }\n}\n"]}
@@ -2,9 +2,22 @@ import { hasOverflow } from '../has-overflow.js';
2
2
  export class TooltipReactiveController {
3
3
  constructor(host, options) {
4
4
  this.handleMouseOver = (e) => {
5
- var _a;
6
- const element = e.target;
7
- if (!((_a = element.hasAttribute) === null || _a === void 0 ? void 0 : _a.call(element, this.attributeName)) || !hasOverflow(element)) {
5
+ var _a, _b;
6
+ /*
7
+ * 표식이 달린 **조상**을 찾는다 — 포인터가 닿는 것은 대개 그 안의 조각이다.
8
+ *
9
+ * `e.target` 만 보고 있었다. 그런데 툴팁을 붙이고 싶은 줄은 거의 언제나 안에 조각을 가진다 —
10
+ * 공장 달력의 한 줄은 [표시 6px][줄머리][제목] 셋이고, data-grist 의 머리말도 안에 아이콘이
11
+ * 들어간다. 포인터가 그 조각 위에 있으면 `e.target` 이 조각이라 표식이 없고, 그대로 돌아갔다.
12
+ *
13
+ * [확인함] 2026-09-11 공장 달력 한 줄 114px 중 제목 조각이 104px.
14
+ * 줄에 직접 쏘면 뜨고, 제목 조각에 쏘면 안 뜬다.
15
+ * **91% 의 자리에서 안 뜬다** — 사실상 안 뜨는 것이다.
16
+ *
17
+ * `closest` 는 자기 자신부터 본다. 표식이 잎에 달린 경우는 전과 똑같이 동작한다.
18
+ */
19
+ const element = (_b = (_a = e.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, `[${this.attributeName}]`);
20
+ if (!element || !hasOverflow(element)) {
8
21
  return;
9
22
  }
10
23
  TooltipReactiveController.cancelPending();
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip-reactive-controller.js","sourceRoot":"","sources":["../../../src/reactive-controllers/tooltip-reactive-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { hasOverflow } from '../has-overflow.js'\n\nexport class TooltipReactiveController implements ReactiveController {\n /**\n * 뜨기까지 기다리는 시간(밀리초).\n *\n * ── 왜 기다리나 ─────────────────────────────────────────────────────────\n * 표를 훑을 때 마우스는 여러 칸을 스쳐 지난다. 지연이 없으면 그 칸마다 툴팁이 떴다 사라져서, 읽으려는\n * 것을 가리고 눈이 따라가야 한다. 지연을 두면 **머문 곳에만** 뜬다 — 스쳐 지난 것과 보려고 멈춘 것을\n * 가른다(hover intent).\n *\n * 450ms 는 관례에 가까운 값이다. 더 짧으면 스치는 것과 구별되지 않고, 더 길면 「안 뜨는 것」처럼\n * 느껴진다.\n *\n * ── 한 번 뜬 뒤에는 기다리지 않는다 ──────────────────────────────────────\n * 이미 떠 있으면 다음 요소로 옮길 때 바로 바꾼다. 한 번 「보고 싶다」를 표현했으니 옆 칸을 볼 때마다\n * 다시 기다리게 하지 않는다.\n */\n private static readonly DEFAULT_ENTER_DELAY = 450\n\n private host: ReactiveControllerHost & HTMLElement\n private attributeName: string\n private content?: string | ((element: HTMLElement) => string)\n private enterDelay: number\n private static tooltipContainer: HTMLElement | null = null\n /** 뜨기를 기다리는 중인 타이머. */\n private static pending: ReturnType<typeof setTimeout> | null = null\n /** 지금 툴팁이 떠 있는가. 떠 있으면 다음 요소로 옮길 때 기다리지 않는다. */\n private static shown = false\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n options?: {\n content?: string | ((element: HTMLElement) => string)\n attributeName?: string\n /** 머무른 시간이 이만큼을 넘으면 뜬다(밀리초). 0 이면 바로 뜬다. */\n enterDelay?: number\n }\n ) {\n const { attributeName, content, enterDelay } = options || {}\n\n this.host = host\n this.attributeName = attributeName || 'data-reactive-tooltip'\n this.content = content\n this.enterDelay = enterDelay ?? TooltipReactiveController.DEFAULT_ENTER_DELAY\n\n host.addController(this)\n\n TooltipReactiveController.ensureContainer()\n }\n\n /**\n * 툴팁 상자를 만든다 — 문서에 하나만 둔다.\n *\n * ── 모양 ────────────────────────────────────────────────────────────────\n * 전에는 검정 70% 바탕 · 3px 둥글기 · 물려받은 글자 크기였다. 앱의 토큰을 하나도 쓰지 않아서, 화면이\n * 어떤 색과 크기를 쓰든 툴팁만 브라우저 기본처럼 보였다.\n *\n * 이제 --ui-* 를 본다. 툴팁은 **화면 위에 잠깐 떠 있는 것**이라 화면과 같은 면이 아니고, 그래서\n * 어두운 면에 흰 글자다 — 밝은 면으로 두면 표와 섞여 어디까지가 툴팁인지 안 보인다.\n *\n * 글자는 본문보다 한 단 작다(12px). 툴팁은 값이 아니라 값을 거드는 말이다.\n *\n * 테두리는 두지 않는다 — 어두운 면이 스스로 경계다. 그림자는 떠 있음을 말할 만큼만 옅게 둔다.\n *\n * 값은 모두 --tooltip-* 으로 덮을 수 있다. 어두운 테마는 이 값들을 뒤집어야 한다.\n */\n private static ensureContainer() {\n if (TooltipReactiveController.tooltipContainer) {\n return\n }\n\n const box = document.createElement('div')\n\n box.className = 'ox-tooltip-container'\n box.style.position = 'fixed'\n box.style.pointerEvents = 'none'\n box.style.zIndex = '1000'\n box.style.display = 'none'\n\n /*\n * 낱개 속성으로 적는다. font 축약형은 글꼴 이름을 반드시 요구해서 inherit 을 넣으면 선언이 통째로\n * 버려진다. 글꼴은 적지 않아도 body 에서 물려받는다 — 이 상자가 body 의 자식이다.\n */\n box.style.fontFamily = 'var(--tooltip-font-family, inherit)'\n box.style.fontSize = 'var(--tooltip-font-size, 12px)'\n box.style.lineHeight = 'var(--tooltip-line-height, 1.45)'\n box.style.fontWeight = 'var(--tooltip-font-weight, 400)'\n box.style.color = 'var(--tooltip-color, #ffffff)'\n box.style.backgroundColor = 'var(--tooltip-background-color, rgba(23, 36, 44, 0.94))'\n box.style.padding = 'var(--tooltip-padding, 6px 8px)'\n box.style.borderRadius = 'var(--tooltip-border-radius, 6px)'\n box.style.boxShadow = 'var(--tooltip-box-shadow, 0 4px 12px rgba(23, 36, 44, 0.18))'\n box.style.maxWidth = 'var(--tooltip-max-width, 280px)'\n box.style.wordWrap = 'break-word'\n box.style.whiteSpace = 'pre-wrap'\n box.style.overflow = 'hidden'\n\n TooltipReactiveController.tooltipContainer = box\n document.body.appendChild(box)\n }\n\n hostConnected() {\n const target = this.host.shadowRoot ?? this.host\n target.addEventListener('mouseover', this.handleMouseOver)\n target.addEventListener('mouseout', this.handleMouseOut)\n }\n\n hostDisconnected() {\n const target = this.host.shadowRoot ?? this.host\n target.removeEventListener('mouseover', this.handleMouseOver)\n target.removeEventListener('mouseout', this.handleMouseOut)\n }\n\n private handleMouseOver = (e: Event) => {\n const element = e.target as HTMLElement\n\n if (!element.hasAttribute?.(this.attributeName) || !hasOverflow(element)) {\n return\n }\n\n TooltipReactiveController.cancelPending()\n\n /* 이미 떠 있으면 기다리지 않는다(위 머리말). */\n if (TooltipReactiveController.shown || this.enterDelay <= 0) {\n this.show(element)\n return\n }\n\n TooltipReactiveController.pending = setTimeout(() => {\n TooltipReactiveController.pending = null\n this.show(element)\n }, this.enterDelay)\n }\n\n private static cancelPending() {\n if (TooltipReactiveController.pending) {\n clearTimeout(TooltipReactiveController.pending)\n TooltipReactiveController.pending = null\n }\n }\n\n private show(element: HTMLElement) {\n {\n const tooltipContent =\n typeof this.content === 'function'\n ? this.content.call(this, element)\n : this.content || element.textContent?.trim() || ''\n\n const rect = element.getBoundingClientRect()\n const box = TooltipReactiveController.tooltipContainer!\n\n /* 내용을 먼저 넣어야 크기를 잴 수 있다. */\n box.textContent = tooltipContent\n box.style.display = 'block'\n box.style.left = '0px'\n box.style.top = '0px'\n\n /*\n * 화면 안으로 밀어 넣는다.\n *\n * 전에는 겨눈 요소의 왼쪽 · 아래에 그냥 놓았다. 그래서 오른쪽 끝 칸이나 마지막 줄에서는 툴팁이\n * 화면 밖으로 나가 **보이지 않았다.** 잘린 글자를 보여 주려고 있는 것인데 그때 안 보인다.\n *\n * 넘치면 반대쪽으로 접는다 — 오른쪽이 모자라면 오른쪽 끝에 맞추고, 아래가 모자라면 요소 위로\n * 올린다.\n */\n const GAP = 4\n const size = box.getBoundingClientRect()\n\n let left = rect.left\n let top = rect.bottom + GAP\n\n if (left + size.width > window.innerWidth - GAP) {\n left = Math.max(GAP, window.innerWidth - size.width - GAP)\n }\n\n if (top + size.height > window.innerHeight - GAP) {\n top = Math.max(GAP, rect.top - size.height - GAP)\n }\n\n box.style.left = `${Math.round(left)}px`\n box.style.top = `${Math.round(top)}px`\n\n TooltipReactiveController.shown = true\n }\n }\n\n private handleMouseOut = (e: Event) => {\n /* 기다리던 것도 취소한다 — 안 그러면 떠난 뒤에 뜬다. */\n TooltipReactiveController.cancelPending()\n\n if (TooltipReactiveController.tooltipContainer) {\n TooltipReactiveController.tooltipContainer.style.display = 'none'\n }\n\n TooltipReactiveController.shown = false\n }\n}\n"]}
1
+ {"version":3,"file":"tooltip-reactive-controller.js","sourceRoot":"","sources":["../../../src/reactive-controllers/tooltip-reactive-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { hasOverflow } from '../has-overflow.js'\n\nexport class TooltipReactiveController implements ReactiveController {\n /**\n * 뜨기까지 기다리는 시간(밀리초).\n *\n * ── 왜 기다리나 ─────────────────────────────────────────────────────────\n * 표를 훑을 때 마우스는 여러 칸을 스쳐 지난다. 지연이 없으면 그 칸마다 툴팁이 떴다 사라져서, 읽으려는\n * 것을 가리고 눈이 따라가야 한다. 지연을 두면 **머문 곳에만** 뜬다 — 스쳐 지난 것과 보려고 멈춘 것을\n * 가른다(hover intent).\n *\n * 450ms 는 관례에 가까운 값이다. 더 짧으면 스치는 것과 구별되지 않고, 더 길면 「안 뜨는 것」처럼\n * 느껴진다.\n *\n * ── 한 번 뜬 뒤에는 기다리지 않는다 ──────────────────────────────────────\n * 이미 떠 있으면 다음 요소로 옮길 때 바로 바꾼다. 한 번 「보고 싶다」를 표현했으니 옆 칸을 볼 때마다\n * 다시 기다리게 하지 않는다.\n */\n private static readonly DEFAULT_ENTER_DELAY = 450\n\n private host: ReactiveControllerHost & HTMLElement\n private attributeName: string\n private content?: string | ((element: HTMLElement) => string)\n private enterDelay: number\n private static tooltipContainer: HTMLElement | null = null\n /** 뜨기를 기다리는 중인 타이머. */\n private static pending: ReturnType<typeof setTimeout> | null = null\n /** 지금 툴팁이 떠 있는가. 떠 있으면 다음 요소로 옮길 때 기다리지 않는다. */\n private static shown = false\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n options?: {\n content?: string | ((element: HTMLElement) => string)\n attributeName?: string\n /** 머무른 시간이 이만큼을 넘으면 뜬다(밀리초). 0 이면 바로 뜬다. */\n enterDelay?: number\n }\n ) {\n const { attributeName, content, enterDelay } = options || {}\n\n this.host = host\n this.attributeName = attributeName || 'data-reactive-tooltip'\n this.content = content\n this.enterDelay = enterDelay ?? TooltipReactiveController.DEFAULT_ENTER_DELAY\n\n host.addController(this)\n\n TooltipReactiveController.ensureContainer()\n }\n\n /**\n * 툴팁 상자를 만든다 — 문서에 하나만 둔다.\n *\n * ── 모양 ────────────────────────────────────────────────────────────────\n * 전에는 검정 70% 바탕 · 3px 둥글기 · 물려받은 글자 크기였다. 앱의 토큰을 하나도 쓰지 않아서, 화면이\n * 어떤 색과 크기를 쓰든 툴팁만 브라우저 기본처럼 보였다.\n *\n * 이제 --ui-* 를 본다. 툴팁은 **화면 위에 잠깐 떠 있는 것**이라 화면과 같은 면이 아니고, 그래서\n * 어두운 면에 흰 글자다 — 밝은 면으로 두면 표와 섞여 어디까지가 툴팁인지 안 보인다.\n *\n * 글자는 본문보다 한 단 작다(12px). 툴팁은 값이 아니라 값을 거드는 말이다.\n *\n * 테두리는 두지 않는다 — 어두운 면이 스스로 경계다. 그림자는 떠 있음을 말할 만큼만 옅게 둔다.\n *\n * 값은 모두 --tooltip-* 으로 덮을 수 있다. 어두운 테마는 이 값들을 뒤집어야 한다.\n */\n private static ensureContainer() {\n if (TooltipReactiveController.tooltipContainer) {\n return\n }\n\n const box = document.createElement('div')\n\n box.className = 'ox-tooltip-container'\n box.style.position = 'fixed'\n box.style.pointerEvents = 'none'\n box.style.zIndex = '1000'\n box.style.display = 'none'\n\n /*\n * 낱개 속성으로 적는다. font 축약형은 글꼴 이름을 반드시 요구해서 inherit 을 넣으면 선언이 통째로\n * 버려진다. 글꼴은 적지 않아도 body 에서 물려받는다 — 이 상자가 body 의 자식이다.\n */\n box.style.fontFamily = 'var(--tooltip-font-family, inherit)'\n box.style.fontSize = 'var(--tooltip-font-size, 12px)'\n box.style.lineHeight = 'var(--tooltip-line-height, 1.45)'\n box.style.fontWeight = 'var(--tooltip-font-weight, 400)'\n box.style.color = 'var(--tooltip-color, #ffffff)'\n box.style.backgroundColor = 'var(--tooltip-background-color, rgba(23, 36, 44, 0.94))'\n box.style.padding = 'var(--tooltip-padding, 6px 8px)'\n box.style.borderRadius = 'var(--tooltip-border-radius, 6px)'\n box.style.boxShadow = 'var(--tooltip-box-shadow, 0 4px 12px rgba(23, 36, 44, 0.18))'\n box.style.maxWidth = 'var(--tooltip-max-width, 280px)'\n box.style.wordWrap = 'break-word'\n box.style.whiteSpace = 'pre-wrap'\n box.style.overflow = 'hidden'\n\n TooltipReactiveController.tooltipContainer = box\n document.body.appendChild(box)\n }\n\n hostConnected() {\n const target = this.host.shadowRoot ?? this.host\n target.addEventListener('mouseover', this.handleMouseOver)\n target.addEventListener('mouseout', this.handleMouseOut)\n }\n\n hostDisconnected() {\n const target = this.host.shadowRoot ?? this.host\n target.removeEventListener('mouseover', this.handleMouseOver)\n target.removeEventListener('mouseout', this.handleMouseOut)\n }\n\n private handleMouseOver = (e: Event) => {\n /*\n * 표식이 달린 **조상**을 찾는다 — 포인터가 닿는 것은 대개 그 안의 조각이다.\n *\n * `e.target` 만 보고 있었다. 그런데 툴팁을 붙이고 싶은 줄은 거의 언제나 안에 조각을 가진다 —\n * 공장 달력의 한 줄은 [표시 6px][줄머리][제목] 셋이고, data-grist 의 머리말도 안에 아이콘이\n * 들어간다. 포인터가 그 조각 위에 있으면 `e.target` 이 조각이라 표식이 없고, 그대로 돌아갔다.\n *\n * [확인함] 2026-09-11 공장 달력 한 줄 114px 중 제목 조각이 104px.\n * 줄에 직접 쏘면 뜨고, 제목 조각에 쏘면 안 뜬다.\n * **91% 의 자리에서 안 뜬다** — 사실상 안 뜨는 것이다.\n *\n * `closest` 는 자기 자신부터 본다. 표식이 잎에 달린 경우는 전과 똑같이 동작한다.\n */\n const element = (e.target as HTMLElement).closest?.(`[${this.attributeName}]`) as HTMLElement\n\n if (!element || !hasOverflow(element)) {\n return\n }\n\n TooltipReactiveController.cancelPending()\n\n /* 이미 떠 있으면 기다리지 않는다(위 머리말). */\n if (TooltipReactiveController.shown || this.enterDelay <= 0) {\n this.show(element)\n return\n }\n\n TooltipReactiveController.pending = setTimeout(() => {\n TooltipReactiveController.pending = null\n this.show(element)\n }, this.enterDelay)\n }\n\n private static cancelPending() {\n if (TooltipReactiveController.pending) {\n clearTimeout(TooltipReactiveController.pending)\n TooltipReactiveController.pending = null\n }\n }\n\n private show(element: HTMLElement) {\n {\n const tooltipContent =\n typeof this.content === 'function'\n ? this.content.call(this, element)\n : this.content || element.textContent?.trim() || ''\n\n const rect = element.getBoundingClientRect()\n const box = TooltipReactiveController.tooltipContainer!\n\n /* 내용을 먼저 넣어야 크기를 잴 수 있다. */\n box.textContent = tooltipContent\n box.style.display = 'block'\n box.style.left = '0px'\n box.style.top = '0px'\n\n /*\n * 화면 안으로 밀어 넣는다.\n *\n * 전에는 겨눈 요소의 왼쪽 · 아래에 그냥 놓았다. 그래서 오른쪽 끝 칸이나 마지막 줄에서는 툴팁이\n * 화면 밖으로 나가 **보이지 않았다.** 잘린 글자를 보여 주려고 있는 것인데 그때 안 보인다.\n *\n * 넘치면 반대쪽으로 접는다 — 오른쪽이 모자라면 오른쪽 끝에 맞추고, 아래가 모자라면 요소 위로\n * 올린다.\n */\n const GAP = 4\n const size = box.getBoundingClientRect()\n\n let left = rect.left\n let top = rect.bottom + GAP\n\n if (left + size.width > window.innerWidth - GAP) {\n left = Math.max(GAP, window.innerWidth - size.width - GAP)\n }\n\n if (top + size.height > window.innerHeight - GAP) {\n top = Math.max(GAP, rect.top - size.height - GAP)\n }\n\n box.style.left = `${Math.round(left)}px`\n box.style.top = `${Math.round(top)}px`\n\n TooltipReactiveController.shown = true\n }\n }\n\n private handleMouseOut = (e: Event) => {\n /* 기다리던 것도 취소한다 — 안 그러면 떠난 뒤에 뜬다. */\n TooltipReactiveController.cancelPending()\n\n if (TooltipReactiveController.tooltipContainer) {\n TooltipReactiveController.tooltipContainer.style.display = 'none'\n }\n\n TooltipReactiveController.shown = false\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+ import { hasOverflow } from '../src/has-overflow.js';
3
+ /**
4
+ * A tooltip that only expands cut text has to know when text is cut.
5
+ *
6
+ * scrollWidth answers that for a block. It does not for a flex row: flex lays its items out to
7
+ * fit the box, so the box reports no overflow even while the title inside it is ellipsised.
8
+ * Measured on the plant calendar - 25 event rows, every title visibly cut, every one reported
9
+ * as not overflowing, and the tooltip never appeared on any of them.
10
+ */
11
+ describe('hasOverflow', () => {
12
+ const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME';
13
+ it('says no when the text fits', async () => {
14
+ const el = (await fixture(html `
15
+ <div style="width: 400px; overflow: hidden; white-space: nowrap">short</div>
16
+ `));
17
+ expect(hasOverflow(el)).to.be.false;
18
+ });
19
+ it('says yes for a block whose text is cut', async () => {
20
+ const el = (await fixture(html `
21
+ <div style="width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">${LONG}</div>
22
+ `));
23
+ expect(hasOverflow(el), 'a block overflows its own box, which scrollWidth reports').to.be.true;
24
+ });
25
+ it('says yes for a flex row whose title span is cut', async () => {
26
+ /*
27
+ * The shape the plant calendar draws: a flex row holding a mark and the title, with the
28
+ * title in its own span that clips. overflow:hidden resolves that span's min-width to 0, so
29
+ * it shrinks to fit and the row around it never reports overflow of its own.
30
+ */
31
+ const el = (await fixture(html `
32
+ <div style="display: flex; gap: 4px; width: 60px; overflow: hidden; white-space: nowrap">
33
+ <span style="flex: none; width: 6px; height: 6px"></span>
34
+ <span style="overflow: hidden; text-overflow: ellipsis">${LONG}</span>
35
+ </div>
36
+ `));
37
+ expect(el.scrollWidth, 'the row reports no overflow of its own — this is why the first check is not enough').to.equal(el.clientWidth);
38
+ expect(hasOverflow(el), 'the title is cut, so a tooltip is worth showing').to.be.true;
39
+ });
40
+ it('says no for a flex row that fits, so tooltips do not fire on everything', async () => {
41
+ const el = (await fixture(html `
42
+ <div style="display: flex; width: 400px; overflow: hidden; white-space: nowrap">
43
+ <span style="flex: none; width: 6px; height: 6px"></span>short
44
+ </div>
45
+ `));
46
+ expect(hasOverflow(el)).to.be.false;
47
+ });
48
+ });
49
+ //# sourceMappingURL=has-overflow.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"has-overflow.test.js","sourceRoot":"","sources":["../../test/has-overflow.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEpD;;;;;;;GAOG;AACH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,MAAM,IAAI,GAAG,sDAAsD,CAAA;IAEnE,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;KAE7B,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAA;IACrC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;iGAC+D,IAAI;KAChG,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,0DAA0D,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAA;IAChG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D;;;;WAIG;QACH,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;;kEAGgC,IAAI;;KAEjE,CAAC,CAAgB,CAAA;QAElB,MAAM,CACJ,EAAE,CAAC,WAAW,EACd,oFAAoF,CACrF,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,CAAA;QAC1B,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,iDAAiD,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAA;IACvF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAA;;;;KAI7B,CAAC,CAAgB,CAAA;QAElB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAA;IACrC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { expect, fixture, html } from '@open-wc/testing'\n\nimport { hasOverflow } from '../src/has-overflow.js'\n\n/**\n * A tooltip that only expands cut text has to know when text is cut.\n *\n * scrollWidth answers that for a block. It does not for a flex row: flex lays its items out to\n * fit the box, so the box reports no overflow even while the title inside it is ellipsised.\n * Measured on the plant calendar - 25 event rows, every title visibly cut, every one reported\n * as not overflowing, and the tooltip never appeared on any of them.\n */\ndescribe('hasOverflow', () => {\n const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME'\n\n it('says no when the text fits', async () => {\n const el = (await fixture(html`\n <div style=\"width: 400px; overflow: hidden; white-space: nowrap\">short</div>\n `)) as HTMLElement\n\n expect(hasOverflow(el)).to.be.false\n })\n\n it('says yes for a block whose text is cut', async () => {\n const el = (await fixture(html`\n <div style=\"width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap\">${LONG}</div>\n `)) as HTMLElement\n\n expect(hasOverflow(el), 'a block overflows its own box, which scrollWidth reports').to.be.true\n })\n\n it('says yes for a flex row whose title span is cut', async () => {\n /*\n * The shape the plant calendar draws: a flex row holding a mark and the title, with the\n * title in its own span that clips. overflow:hidden resolves that span's min-width to 0, so\n * it shrinks to fit and the row around it never reports overflow of its own.\n */\n const el = (await fixture(html`\n <div style=\"display: flex; gap: 4px; width: 60px; overflow: hidden; white-space: nowrap\">\n <span style=\"flex: none; width: 6px; height: 6px\"></span>\n <span style=\"overflow: hidden; text-overflow: ellipsis\">${LONG}</span>\n </div>\n `)) as HTMLElement\n\n expect(\n el.scrollWidth,\n 'the row reports no overflow of its own — this is why the first check is not enough'\n ).to.equal(el.clientWidth)\n expect(hasOverflow(el), 'the title is cut, so a tooltip is worth showing').to.be.true\n })\n\n it('says no for a flex row that fits, so tooltips do not fire on everything', async () => {\n const el = (await fixture(html`\n <div style=\"display: flex; width: 400px; overflow: hidden; white-space: nowrap\">\n <span style=\"flex: none; width: 6px; height: 6px\"></span>short\n </div>\n `)) as HTMLElement\n\n expect(hasOverflow(el)).to.be.false\n })\n})\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { hasMore, listCountLabel, listPhase } from '../src/list-state.js';
3
+ /**
4
+ * The list-state rule, carried over from operato-twin with its cases. The keys are written out as
5
+ * text so a rename here shows up as a decision, not as a silent follow.
6
+ */
7
+ describe('listPhase', () => {
8
+ it('an error wins over everything, even rows in hand', () => {
9
+ expect(listPhase({ shown: 3, loaded: 3 }, { error: new Error('x') })).to.equal('error');
10
+ });
11
+ it('loading only while nothing is held', () => {
12
+ expect(listPhase({ shown: 0, loaded: 0 }, { loading: true })).to.equal('loading');
13
+ expect(listPhase({ shown: 2, loaded: 2 }, { loading: true })).to.equal('ready');
14
+ });
15
+ it('tells empty from empty-because-narrowed', () => {
16
+ expect(listPhase({ shown: 0, loaded: 0 })).to.equal('empty');
17
+ expect(listPhase({ shown: 0, loaded: 5, filtered: true })).to.equal('empty-filtered');
18
+ });
19
+ });
20
+ describe('hasMore', () => {
21
+ it('uses the total when it is known', () => {
22
+ expect(hasMore({ shown: 10, loaded: 10, total: 12 })).to.equal(true);
23
+ expect(hasMore({ shown: 12, loaded: 12, total: 12 })).to.equal(false);
24
+ });
25
+ it('reads an unknown total with a capped list as more, not as none', () => {
26
+ expect(hasMore({ shown: 100, loaded: 100, capped: true })).to.equal(true);
27
+ expect(hasMore({ shown: 100, loaded: 100 })).to.equal(false);
28
+ });
29
+ });
30
+ describe('listCountLabel', () => {
31
+ it('says the cap when the total is unknown', () => {
32
+ expect(listCountLabel({ shown: 100, loaded: 100, capped: true })).to.deep.equal({
33
+ key: 'text.list-count-capped',
34
+ params: { loaded: 100 }
35
+ });
36
+ });
37
+ it('says shown of total when the server has more', () => {
38
+ expect(listCountLabel({ shown: 48, loaded: 48, total: 12904 })).to.deep.equal({
39
+ key: 'text.list-count-of-total',
40
+ params: { shown: 48, total: 12904 }
41
+ });
42
+ });
43
+ it('says shown of loaded when narrowed', () => {
44
+ expect(listCountLabel({ shown: 3, loaded: 40, filtered: true })).to.deep.equal({
45
+ key: 'text.list-count-of-loaded',
46
+ params: { shown: 3, loaded: 40 }
47
+ });
48
+ });
49
+ it('says the plain count otherwise', () => {
50
+ expect(listCountLabel({ shown: 7, loaded: 7 })).to.deep.equal({ key: 'text.list-count', params: { shown: 7 } });
51
+ });
52
+ });
53
+ //# sourceMappingURL=list-state.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-state.test.js","sourceRoot":"","sources":["../../test/list-state.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAEzE;;;GAGG;AACH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACzF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QACjF,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACjF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC5D,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACvF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACpE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACzE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YAC9E,GAAG,EAAE,wBAAwB;YAC7B,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;SACxB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YAC5E,GAAG,EAAE,0BAA0B;YAC/B,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;SACpC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YAC7E,GAAG,EAAE,2BAA2B;YAChC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;SACjC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;IACjH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport { hasMore, listCountLabel, listPhase } from '../src/list-state.js'\n\n/**\n * The list-state rule, carried over from operato-twin with its cases. The keys are written out as\n * text so a rename here shows up as a decision, not as a silent follow.\n */\ndescribe('listPhase', () => {\n it('an error wins over everything, even rows in hand', () => {\n expect(listPhase({ shown: 3, loaded: 3 }, { error: new Error('x') })).to.equal('error')\n })\n\n it('loading only while nothing is held', () => {\n expect(listPhase({ shown: 0, loaded: 0 }, { loading: true })).to.equal('loading')\n expect(listPhase({ shown: 2, loaded: 2 }, { loading: true })).to.equal('ready')\n })\n\n it('tells empty from empty-because-narrowed', () => {\n expect(listPhase({ shown: 0, loaded: 0 })).to.equal('empty')\n expect(listPhase({ shown: 0, loaded: 5, filtered: true })).to.equal('empty-filtered')\n })\n})\n\ndescribe('hasMore', () => {\n it('uses the total when it is known', () => {\n expect(hasMore({ shown: 10, loaded: 10, total: 12 })).to.equal(true)\n expect(hasMore({ shown: 12, loaded: 12, total: 12 })).to.equal(false)\n })\n\n it('reads an unknown total with a capped list as more, not as none', () => {\n expect(hasMore({ shown: 100, loaded: 100, capped: true })).to.equal(true)\n expect(hasMore({ shown: 100, loaded: 100 })).to.equal(false)\n })\n})\n\ndescribe('listCountLabel', () => {\n it('says the cap when the total is unknown', () => {\n expect(listCountLabel({ shown: 100, loaded: 100, capped: true })).to.deep.equal({\n key: 'text.list-count-capped',\n params: { loaded: 100 }\n })\n })\n\n it('says shown of total when the server has more', () => {\n expect(listCountLabel({ shown: 48, loaded: 48, total: 12904 })).to.deep.equal({\n key: 'text.list-count-of-total',\n params: { shown: 48, total: 12904 }\n })\n })\n\n it('says shown of loaded when narrowed', () => {\n expect(listCountLabel({ shown: 3, loaded: 40, filtered: true })).to.deep.equal({\n key: 'text.list-count-of-loaded',\n params: { shown: 3, loaded: 40 }\n })\n })\n\n it('says the plain count otherwise', () => {\n expect(listCountLabel({ shown: 7, loaded: 7 })).to.deep.equal({ key: 'text.list-count', params: { shown: 7 } })\n })\n})\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { __decorate } from "tslib";
2
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
+ import { LitElement, html as litHtml } from 'lit';
4
+ import { customElement } from 'lit/decorators.js';
5
+ import { TooltipReactiveController } from '../src/reactive-controllers/tooltip-reactive-controller.js';
6
+ const LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME';
7
+ /**
8
+ * A row of the shape hosts actually draw: a marker, a prefix and the title, each in its own
9
+ * span. The pointer lands on one of those children, never on the row itself - on the plant
10
+ * calendar the title span alone covers 104 of the row's 114px.
11
+ */
12
+ let TooltipHostFixture = class TooltipHostFixture extends LitElement {
13
+ constructor() {
14
+ super(...arguments);
15
+ /*
16
+ * The content function the plant calendar passes: the host writes the sentence it wants read
17
+ * into the attribute, which is more than the row's own cut text.
18
+ */
19
+ this.tooltip = new TooltipReactiveController(this, {
20
+ enterDelay: 0,
21
+ content: element => { var _a; return element.getAttribute('data-reactive-tooltip') || ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; }
22
+ });
23
+ }
24
+ render() {
25
+ return litHtml `
26
+ <div
27
+ data-reactive-tooltip="the whole sentence"
28
+ style="display:flex; gap:4px; width:60px; overflow:hidden; white-space:nowrap"
29
+ >
30
+ <span mark style="flex:none; width:6px; height:6px"></span>
31
+ <span title-part style="overflow:hidden; text-overflow:ellipsis">${LONG}</span>
32
+ </div>
33
+ `;
34
+ }
35
+ };
36
+ TooltipHostFixture = __decorate([
37
+ customElement('tooltip-host-fixture')
38
+ ], TooltipHostFixture);
39
+ function box() {
40
+ return document.querySelector('.ox-tooltip-container');
41
+ }
42
+ function hover(el) {
43
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, composed: true }));
44
+ }
45
+ describe('TooltipReactiveController', () => {
46
+ let host;
47
+ beforeEach(async () => {
48
+ host = (await fixture(html `<tooltip-host-fixture></tooltip-host-fixture>`));
49
+ box().style.display = 'none';
50
+ });
51
+ it('shows when the pointer is over a child of the marked row', async () => {
52
+ /*
53
+ * The regression this file exists for. The controller read the attribute off e.target, and
54
+ * the pointer is over a child for all but a few pixels of the row - so the tooltip never
55
+ * appeared on the plant calendar, on any row.
56
+ */
57
+ hover(host.shadowRoot.querySelector('[title-part]'));
58
+ await waitUntil(() => box().style.display === 'block');
59
+ expect(box().textContent).to.equal('the whole sentence');
60
+ });
61
+ it('shows when the pointer is over the marked row itself', async () => {
62
+ hover(host.shadowRoot.querySelector('[data-reactive-tooltip]'));
63
+ await waitUntil(() => box().style.display === 'block');
64
+ });
65
+ it('reads the attribute of the marked ancestor, not the child under the pointer', async () => {
66
+ hover(host.shadowRoot.querySelector('[mark]'));
67
+ await waitUntil(() => box().style.display === 'block');
68
+ expect(box().textContent, 'the marker span holds no text of its own').to.equal('the whole sentence');
69
+ });
70
+ it('stays hidden when nothing in the ancestry is marked', async () => {
71
+ const plain = (await fixture(html `<span>${LONG}</span>`));
72
+ host.shadowRoot.appendChild(plain);
73
+ hover(plain);
74
+ await new Promise(resolve => setTimeout(resolve, 50));
75
+ expect(box().style.display).to.equal('none');
76
+ });
77
+ });
78
+ //# sourceMappingURL=tooltip-reactive-controller.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip-reactive-controller.test.js","sourceRoot":"","sources":["../../test/tooltip-reactive-controller.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture, html, waitUntil } from '@open-wc/testing'\nimport { LitElement, html as litHtml } from 'lit'\nimport { customElement } from 'lit/decorators.js'\n\nimport { TooltipReactiveController } from '../src/reactive-controllers/tooltip-reactive-controller.js'\n\nconst LONG = 'CAM-LEAK-VISUAL-INSPECTION-STATION-284-AND-THEN-SOME'\n\n/**\n * A row of the shape hosts actually draw: a marker, a prefix and the title, each in its own\n * span. The pointer lands on one of those children, never on the row itself - on the plant\n * calendar the title span alone covers 104 of the row's 114px.\n */\n@customElement('tooltip-host-fixture')\nclass TooltipHostFixture extends LitElement {\n /*\n * The content function the plant calendar passes: the host writes the sentence it wants read\n * into the attribute, which is more than the row's own cut text.\n */\n tooltip = new TooltipReactiveController(this, {\n enterDelay: 0,\n content: element => element.getAttribute('data-reactive-tooltip') || element.textContent?.trim() || ''\n })\n\n render() {\n return litHtml`\n <div\n data-reactive-tooltip=\"the whole sentence\"\n style=\"display:flex; gap:4px; width:60px; overflow:hidden; white-space:nowrap\"\n >\n <span mark style=\"flex:none; width:6px; height:6px\"></span>\n <span title-part style=\"overflow:hidden; text-overflow:ellipsis\">${LONG}</span>\n </div>\n `\n }\n}\n\nfunction box(): HTMLElement {\n return document.querySelector('.ox-tooltip-container') as HTMLElement\n}\n\nfunction hover(el: Element) {\n el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, composed: true }))\n}\n\ndescribe('TooltipReactiveController', () => {\n let host: TooltipHostFixture\n\n beforeEach(async () => {\n host = (await fixture(html`<tooltip-host-fixture></tooltip-host-fixture>`)) as TooltipHostFixture\n box().style.display = 'none'\n })\n\n it('shows when the pointer is over a child of the marked row', async () => {\n /*\n * The regression this file exists for. The controller read the attribute off e.target, and\n * the pointer is over a child for all but a few pixels of the row - so the tooltip never\n * appeared on the plant calendar, on any row.\n */\n hover(host.shadowRoot!.querySelector('[title-part]')!)\n\n await waitUntil(() => box().style.display === 'block')\n expect(box().textContent).to.equal('the whole sentence')\n })\n\n it('shows when the pointer is over the marked row itself', async () => {\n hover(host.shadowRoot!.querySelector('[data-reactive-tooltip]')!)\n\n await waitUntil(() => box().style.display === 'block')\n })\n\n it('reads the attribute of the marked ancestor, not the child under the pointer', async () => {\n hover(host.shadowRoot!.querySelector('[mark]')!)\n\n await waitUntil(() => box().style.display === 'block')\n expect(box().textContent, 'the marker span holds no text of its own').to.equal('the whole sentence')\n })\n\n it('stays hidden when nothing in the ancestry is marked', async () => {\n const plain = (await fixture(html`<span>${LONG}</span>`)) as HTMLElement\n host.shadowRoot!.appendChild(plain)\n\n hover(plain)\n\n await new Promise(resolve => setTimeout(resolve, 50))\n expect(box().style.display).to.equal('none')\n })\n})\n"]}