gridsmith-ui 0.29.1 → 0.30.1

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 (108) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -1
  3. package/dist/DropdownMenu-DqT7ZQ0D.d.ts +39 -0
  4. package/dist/charts.js +24 -9
  5. package/dist/charts.js.map +1 -1
  6. package/dist/chat.js +7 -7
  7. package/dist/{chunk-MFAWJSMJ.js → chunk-6C2IUMY7.js} +4 -23
  8. package/dist/chunk-6C2IUMY7.js.map +1 -0
  9. package/dist/{chunk-OYNUU346.js → chunk-7QLXYRPD.js} +3 -12
  10. package/dist/chunk-7QLXYRPD.js.map +1 -0
  11. package/dist/{chunk-FD4YED7O.js → chunk-DBHVJ7Z3.js} +5 -49
  12. package/dist/chunk-DBHVJ7Z3.js.map +1 -0
  13. package/dist/chunk-IELIF4PJ.js +110 -0
  14. package/dist/chunk-IELIF4PJ.js.map +1 -0
  15. package/dist/chunk-ISONVOA7.js +316 -0
  16. package/dist/chunk-ISONVOA7.js.map +1 -0
  17. package/dist/{chunk-4TUJXDRU.js → chunk-KMKBOTFR.js} +93 -32
  18. package/dist/chunk-KMKBOTFR.js.map +1 -0
  19. package/dist/{chunk-MKFWHWBK.js → chunk-L6HWACK7.js} +7 -7
  20. package/dist/chunk-L6HWACK7.js.map +1 -0
  21. package/dist/chunk-LJYSVZMI.js +25 -0
  22. package/dist/chunk-LJYSVZMI.js.map +1 -0
  23. package/dist/{chunk-6A5RHW2W.js → chunk-NEUWZUK6.js} +4 -5
  24. package/dist/chunk-NEUWZUK6.js.map +1 -0
  25. package/dist/chunk-NKUCS67T.js +13 -0
  26. package/dist/chunk-NKUCS67T.js.map +1 -0
  27. package/dist/{chunk-RECCCA7Z.js → chunk-OVHBQT3Z.js} +6 -5
  28. package/dist/chunk-OVHBQT3Z.js.map +1 -0
  29. package/dist/{chunk-I3MTQ3CS.js → chunk-OX3NNMWG.js} +6 -6
  30. package/dist/{chunk-I3MTQ3CS.js.map → chunk-OX3NNMWG.js.map} +1 -1
  31. package/dist/{chunk-ZI3OQDET.js → chunk-R5QXMNW7.js} +297 -15
  32. package/dist/chunk-R5QXMNW7.js.map +1 -0
  33. package/dist/chunk-SJZWEBHL.js +58 -0
  34. package/dist/chunk-SJZWEBHL.js.map +1 -0
  35. package/dist/{chunk-FUKSLQ77.js → chunk-SLRTSBCJ.js} +91 -51
  36. package/dist/chunk-SLRTSBCJ.js.map +1 -0
  37. package/dist/chunk-T52LAFKG.js +1350 -0
  38. package/dist/chunk-T52LAFKG.js.map +1 -0
  39. package/dist/chunk-TYNDXMBV.js +105 -0
  40. package/dist/chunk-TYNDXMBV.js.map +1 -0
  41. package/dist/{chunk-37WOC7F7.js → chunk-UDJOIVKQ.js} +31 -113
  42. package/dist/chunk-UDJOIVKQ.js.map +1 -0
  43. package/dist/{chunk-RJEUQ7RL.js → chunk-ULCACQSA.js} +47 -90
  44. package/dist/chunk-ULCACQSA.js.map +1 -0
  45. package/dist/{chunk-HP7WUZPK.js → chunk-WIUGPZOH.js} +10 -3
  46. package/dist/chunk-WIUGPZOH.js.map +1 -0
  47. package/dist/{chunk-JTU7OBYJ.js → chunk-YL5KD5RC.js} +4 -3
  48. package/dist/chunk-YL5KD5RC.js.map +1 -0
  49. package/dist/data-grid.d.ts +25 -6
  50. package/dist/data-grid.js +180 -58
  51. package/dist/data-grid.js.map +1 -1
  52. package/dist/editor-mentions.js +4 -5
  53. package/dist/editor-mentions.js.map +1 -1
  54. package/dist/editor.js +11 -8
  55. package/dist/editor.js.map +1 -1
  56. package/dist/{feather-FNBQ74RR.js → feather-XFFEFLTV.js} +5 -3
  57. package/dist/feather-XFFEFLTV.js.map +1 -0
  58. package/dist/{heroicons-DODCSYWD.js → heroicons-L6DOHBEA.js} +5 -3
  59. package/dist/heroicons-L6DOHBEA.js.map +1 -0
  60. package/dist/icons.d.ts +1 -1
  61. package/dist/icons.js +1 -1
  62. package/dist/index.d.ts +138 -39
  63. package/dist/index.js +846 -1667
  64. package/dist/index.js.map +1 -1
  65. package/dist/kanban.d.ts +26 -8
  66. package/dist/kanban.js +204 -79
  67. package/dist/kanban.js.map +1 -1
  68. package/dist/{lucide-ZWE3EPGP.js → lucide-JOORDTO6.js} +5 -3
  69. package/dist/lucide-JOORDTO6.js.map +1 -0
  70. package/dist/{phosphor-DIHPA4AJ.js → phosphor-TKGDYNGR.js} +5 -3
  71. package/dist/phosphor-TKGDYNGR.js.map +1 -0
  72. package/dist/resizable.js +47 -16
  73. package/dist/resizable.js.map +1 -1
  74. package/dist/roots.css +12 -8
  75. package/dist/{table-utils-CxnpFHRF.d.ts → table-utils-BXkP-2Hx.d.ts} +3 -1
  76. package/dist/{tabler-D3VYTSGI.js → tabler-YGETLYRU.js} +5 -3
  77. package/dist/tabler-YGETLYRU.js.map +1 -0
  78. package/dist/tokens.css +14 -0
  79. package/dist/utils.js.map +1 -1
  80. package/dist/virtualized.d.ts +6 -1
  81. package/dist/virtualized.js +17 -13
  82. package/dist/virtualized.js.map +1 -1
  83. package/dist-static/tokens.css +14 -0
  84. package/package.json +1 -1
  85. package/dist/chunk-37WOC7F7.js.map +0 -1
  86. package/dist/chunk-4TUJXDRU.js.map +0 -1
  87. package/dist/chunk-6A5RHW2W.js.map +0 -1
  88. package/dist/chunk-FD4YED7O.js.map +0 -1
  89. package/dist/chunk-FUKSLQ77.js.map +0 -1
  90. package/dist/chunk-HP7WUZPK.js.map +0 -1
  91. package/dist/chunk-JTU7OBYJ.js.map +0 -1
  92. package/dist/chunk-MFAWJSMJ.js.map +0 -1
  93. package/dist/chunk-MKFWHWBK.js.map +0 -1
  94. package/dist/chunk-OYNUU346.js.map +0 -1
  95. package/dist/chunk-QM4SJKWV.js +0 -51
  96. package/dist/chunk-QM4SJKWV.js.map +0 -1
  97. package/dist/chunk-QNHVPE2B.js +0 -117
  98. package/dist/chunk-QNHVPE2B.js.map +0 -1
  99. package/dist/chunk-RECCCA7Z.js.map +0 -1
  100. package/dist/chunk-RJEUQ7RL.js.map +0 -1
  101. package/dist/chunk-WH2FUFAC.js +0 -15
  102. package/dist/chunk-WH2FUFAC.js.map +0 -1
  103. package/dist/chunk-ZI3OQDET.js.map +0 -1
  104. package/dist/feather-FNBQ74RR.js.map +0 -1
  105. package/dist/heroicons-DODCSYWD.js.map +0 -1
  106. package/dist/lucide-ZWE3EPGP.js.map +0 -1
  107. package/dist/phosphor-DIHPA4AJ.js.map +0 -1
  108. package/dist/tabler-D3VYTSGI.js.map +0 -1
package/dist/chat.js CHANGED
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import { Divider, CodeBlock, Link } from './chunk-I3MTQ3CS.js';
3
- import './chunk-RECCCA7Z.js';
4
- import './chunk-37WOC7F7.js';
2
+ import { Divider, CodeBlock, Link } from './chunk-OX3NNMWG.js';
3
+ import './chunk-OVHBQT3Z.js';
4
+ import './chunk-IELIF4PJ.js';
5
+ import './chunk-UDJOIVKQ.js';
6
+ import './chunk-L6HWACK7.js';
5
7
  import './chunk-7Q46JXSK.js';
6
- import './chunk-MKFWHWBK.js';
7
- import './chunk-HP7WUZPK.js';
8
- import './chunk-WH2FUFAC.js';
9
- import { cn } from './chunk-JTU7OBYJ.js';
8
+ import './chunk-WIUGPZOH.js';
9
+ import { cn } from './chunk-YL5KD5RC.js';
10
10
  import { memo, Children, isValidElement } from 'react';
11
11
  import Markdown from 'react-markdown';
12
12
  import remarkGfm from 'remark-gfm';
@@ -1,26 +1,7 @@
1
1
  "use client";
2
- import { FIXED_FORM, localeFor, browserLanguages, formatLocale } from './chunk-OYNUU346.js';
3
- import { cn } from './chunk-JTU7OBYJ.js';
4
- import { createContext, useSyncExternalStore, useContext } from 'react';
2
+ import { cn } from './chunk-YL5KD5RC.js';
5
3
  import { jsx, jsxs } from 'react/jsx-runtime';
6
4
 
7
- var noSubscription = () => () => {
8
- };
9
- var inBrowser = () => true;
10
- var onServer = () => false;
11
- function useHydrated() {
12
- return useSyncExternalStore(noSubscription, inBrowser, onServer);
13
- }
14
- function useToday() {
15
- return useHydrated() ? /* @__PURE__ */ new Date() : null;
16
- }
17
- var LocaleContext = createContext(null);
18
- function useFormatLocale() {
19
- const choice = useContext(LocaleContext);
20
- const hydrated = useHydrated();
21
- if (!hydrated) return choice ? choice.server : FIXED_FORM;
22
- return choice ? localeFor(choice.lang, browserLanguages()) ?? choice.server : formatLocale();
23
- }
24
5
  function Bone({ className, style, "data-gs": marker }) {
25
6
  return /* @__PURE__ */ jsx(
26
7
  "div",
@@ -60,6 +41,6 @@ function Skeleton({ variant = "text", lines = 3, width, height, className }) {
60
41
  // ../../showcase/src/lib/title-text.ts
61
42
  var TITLE_TEXT = "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] break-words";
62
43
 
63
- export { LocaleContext, Skeleton, TITLE_TEXT, useFormatLocale, useHydrated, useToday };
64
- //# sourceMappingURL=chunk-MFAWJSMJ.js.map
65
- //# sourceMappingURL=chunk-MFAWJSMJ.js.map
44
+ export { Skeleton, TITLE_TEXT };
45
+ //# sourceMappingURL=chunk-6C2IUMY7.js.map
46
+ //# sourceMappingURL=chunk-6C2IUMY7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../showcase/src/components/Skeleton.tsx","../../../showcase/src/lib/title-text.ts"],"names":[],"mappings":";;;;AAUA,SAAS,KAAK,EAAE,SAAA,EAAW,KAAA,EAAO,SAAA,EAAW,QAAO,EAA4E;AAC9H,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAS,MAAA;AAAA,MACT,SAAA,EAAW,EAAA,CAAG,wEAAA,EAA0E,SAAS,CAAA;AAAA,MACjG;AAAA;AAAA,GACF;AAEJ;AAEO,SAAS,QAAA,CAAS,EAAE,OAAA,GAAU,MAAA,EAAQ,QAAQ,CAAA,EAAG,KAAA,EAAO,MAAA,EAAQ,SAAA,EAAU,EAAkB;AACjG,EAAA,IAAI,YAAY,QAAA,EAAU;AACxB,IAAA,uBAAO,GAAA,CAAC,QAAK,SAAA,EAAQ,UAAA,EAAW,WAAsB,KAAA,EAAO,EAAE,KAAA,EAAO,MAAA,EAAO,EAAG,CAAA;AAAA,EAClF;AAEA,EAAA,IAAI,YAAY,QAAA,EAAU;AACxB,IAAA,uBAAO,GAAA,CAAC,QAAK,SAAA,EAAQ,UAAA,EAAW,WAAW,EAAA,CAAG,2CAAA,EAA6C,SAAS,CAAA,EAAG,CAAA;AAAA,EACzG;AAEA,EAAA,IAAI,YAAY,MAAA,EAAQ;AACtB,IAAA,uBACE,IAAA,CAAC,SAAI,SAAA,EAAQ,UAAA,EAAW,WAAW,EAAA,CAAG,cAAA,EAAgB,SAAS,CAAA,EAC7D,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,IAAA,EAAA,EAAK,WAAU,2CAAA,EAA4C,CAAA;AAAA,sBAC5D,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,WAAA,EAAY,CAAA;AAAA,sBAC5B,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,WAAA,EAAY;AAAA,KAAA,EAC9B,CAAA;AAAA,EAEJ;AAEA,EAAA,IAAI,YAAY,WAAA,EAAa;AAC3B,IAAA,uBACE,IAAA,CAAC,SAAI,SAAA,EAAQ,UAAA,EAAW,WAAW,EAAA,CAAG,eAAA,EAAiB,SAAS,CAAA,EAC9D,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,IAAA,EAAA,EAAK,WAAU,cAAA,EAAe,CAAA;AAAA,sBAC/B,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,cAAA,EAAe,CAAA;AAAA,sBAC/B,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,YAAA,EAAa,CAAA;AAAA,sBAC7B,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,YAAA,EAAa;AAAA,KAAA,EAC/B,CAAA;AAAA,EAEJ;AAGA,EAAA,MAAM,SAAS,CAAC,OAAA,EAAS,OAAA,EAAS,OAAA,EAAS,SAAS,OAAO,CAAA;AAC3D,EAAA,uBACE,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAQ,UAAA,EAAW,SAAA,EAAW,EAAA,CAAG,cAAA,EAAgB,SAAS,CAAA,EAC5D,QAAA,EAAA,KAAA,CAAM,IAAA,CAAK,EAAE,QAAQ,KAAA,EAAM,EAAG,CAAC,CAAA,EAAG,CAAA,qBACjC,GAAA,CAAC,IAAA,EAAA,EAAa,SAAA,EAAW,GAAG,KAAA,EAAO,MAAA,CAAO,CAAA,GAAI,MAAA,CAAO,MAAM,CAAC,CAAA,EAAA,EAAjD,CAAoD,CAChE,CAAA,EACH,CAAA;AAEJ;;;ACrDO,IAAM,UAAA,GAAa","file":"chunk-6C2IUMY7.js","sourcesContent":["import { cn } from \"../lib/utils\";\n\ninterface SkeletonProps {\n variant?: \"text\" | \"avatar\" | \"card\" | \"table-row\" | \"custom\";\n lines?: number;\n width?: string;\n height?: string;\n className?: string;\n}\n\nfunction Bone({ className, style, \"data-gs\": marker }: { className?: string; style?: React.CSSProperties; \"data-gs\"?: string }) {\n return (\n <div\n data-gs={marker}\n className={cn(\"rounded-[var(--ds-radius-sm)] bg-[var(--ds-bg-emphasis)] animate-pulse\", className)}\n style={style}\n />\n );\n}\n\nexport function Skeleton({ variant = \"text\", lines = 3, width, height, className }: SkeletonProps) {\n if (variant === \"custom\") {\n return <Bone data-gs=\"skeleton\" className={className} style={{ width, height }} />;\n }\n\n if (variant === \"avatar\") {\n return <Bone data-gs=\"skeleton\" className={cn(\"h-10 w-10 rounded-[var(--ds-radius-full)]\", className)} />;\n }\n\n if (variant === \"card\") {\n return (\n <div data-gs=\"skeleton\" className={cn(\"space-y-ds-3\", className)}>\n <Bone className=\"h-32 w-full rounded-[var(--ds-radius-md)]\" />\n <Bone className=\"h-4 w-3/4\" />\n <Bone className=\"h-4 w-1/2\" />\n </div>\n );\n }\n\n if (variant === \"table-row\") {\n return (\n <div data-gs=\"skeleton\" className={cn(\"flex gap-ds-4\", className)}>\n <Bone className=\"h-4 flex-[3]\" />\n <Bone className=\"h-4 flex-[2]\" />\n <Bone className=\"h-4 flex-1\" />\n <Bone className=\"h-4 flex-1\" />\n </div>\n );\n }\n\n // text variant\n const widths = [\"w-3/4\", \"w-1/2\", \"w-5/6\", \"w-2/3\", \"w-4/5\"];\n return (\n <div data-gs=\"skeleton\" className={cn(\"space-y-ds-3\", className)}>\n {Array.from({ length: lines }, (_, i) => (\n <Bone key={i} className={cn(\"h-4\", widths[i % widths.length])} />\n ))}\n </div>\n );\n}\n","/**\n * A title's text classes: CardTitle's, which a titled Table's band and a ChartContainer's title share (Sprint 25.9, A1),\n * one definition so they cannot drift. A word or a name longer than its line breaks at the line's edge (round 27,\n * R27-16: run 70's \"primary-logo-navy.svg\" ran out of its card); break-word, not anywhere, so a title's words still\n * set its minimum and the actions beside it wrap under it before a word is broken (HEADER_LAYOUT.floor).\n */\nexport const TITLE_TEXT = \"font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] break-words\";\n"]}
@@ -1,13 +1,4 @@
1
1
  "use client";
2
- // ../../showcase/src/lib/element-type.ts
3
- var LAZY = /* @__PURE__ */ Symbol.for("react.lazy");
4
- var isLazy = (type) => type !== null && typeof type === "object" && type.$$typeof === LAZY && typeof type._init === "function";
5
- function elementType(type) {
6
- let resolved = type;
7
- for (let step = 0; step < 8 && isLazy(resolved); step++) resolved = resolved._init(resolved._payload);
8
- return resolved;
9
- }
10
-
11
2
  // ../../showcase/src/lib/format-locale.ts
12
3
  var last = null;
13
4
  var FIXED_FORM = "en";
@@ -73,6 +64,6 @@ function choose(page, spoken) {
73
64
  }
74
65
  }
75
66
 
76
- export { FIXED_FORM, browserLanguages, elementType, formatLocale, localeChoice, localeFor };
77
- //# sourceMappingURL=chunk-OYNUU346.js.map
78
- //# sourceMappingURL=chunk-OYNUU346.js.map
67
+ export { FIXED_FORM, browserLanguages, formatLocale, localeChoice, localeFor };
68
+ //# sourceMappingURL=chunk-7QLXYRPD.js.map
69
+ //# sourceMappingURL=chunk-7QLXYRPD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../showcase/src/lib/format-locale.ts"],"names":[],"mappings":"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","file":"chunk-7QLXYRPD.js","sourcesContent":["/** The last page language and visitor's languages read, and the locale they gave: formatting runs often, they change rarely. */\nlet last: { key: string; locale: string | undefined } | null = null;\n\n/**\n * The form a server-rendered page is written in without LocaleProvider, and what the library writes on a server where no\n * locale is given: an explicit locale, the same on every machine and on both sides of hydration (Sprint 25.20 D2).\n */\nexport const FIXED_FORM = \"en\";\n\n/**\n * The locale the library writes dates, times and numbers in: the app's language, the page's `lang`, in the visitor's own\n * formats where they speak it (Sprint 25.17, the owner's decision of 2 October). An English page shows \"5 minutes ago\"\n * and \"250K\" to a visitor whose browser is Japanese, where it showed \"5 分前\" and \"25万\" in a page of English (WCAG 3.1.2:\n * text in the page's language needs no `lang` of its own), and a British visitor to it keeps \"1 Oct 2026\". The visitor's\n * form counts only in the page's script: a page in traditional Chinese stays traditional for a visitor who reads\n * simplified. A page with no `lang` is written in the browser's language, as before. On a server, where there is no page,\n * English (\"en\"), never the machine's own language (Sprint 25.22's review): server code passes requestLocale's choice.\n */\nexport function formatLocale(): string | undefined {\n if (typeof document === \"undefined\") return FIXED_FORM;\n const page = document.documentElement.lang?.trim();\n if (!page) return undefined;\n return localeFor(page, browserLanguages());\n}\n\n/** The browser's languages, in its order (`navigator.languages`), or none where there is no browser. */\nexport function browserLanguages(): readonly string[] {\n if (typeof navigator === \"undefined\") return [];\n return navigator.languages?.length ? navigator.languages : navigator.language ? [navigator.language] : [];\n}\n\n/**\n * The locale for a page language and the languages a visitor speaks, in their order: the visitor's form of the page's\n * language where they speak it, else the page's own (Sprint 25.20: the same choice on a server, from the request's\n * languages, and in the browser). Undefined for a page language Intl can't read.\n */\nexport function localeFor(page: string, spoken: readonly string[]): string | undefined {\n const key = `${page}|${spoken.join(\",\")}`;\n if (last?.key !== key) last = { key, locale: choose(page, spoken) };\n return last.locale;\n}\n\n/** What a server-rendered app hands the library at its top: the page's language, and the visitor's from the request. */\nexport interface LocaleChoice {\n /** The page's language, as `<html lang>` says, or \"en\". */\n lang: string;\n /** The request's languages, in a browser's order; empty without the header. */\n spoken: string[];\n /** Whether the request's header was given. */\n hasHeader: boolean;\n /** The locale the server writes in, and the browser while it hydrates. */\n server: string;\n}\n\n/** What LocaleProvider hands down for a page language and a request's Accept-Language header. */\nexport function localeChoice(lang: string | undefined, acceptLanguage: string | null | undefined): LocaleChoice {\n const page = lang?.trim() || FIXED_FORM;\n const spoken = acceptedLanguages(acceptLanguage);\n return { lang: page, spoken, hasHeader: !!acceptLanguage, server: localeFor(page, spoken) ?? FIXED_FORM };\n}\n\n/**\n * The locale LocaleProvider writes in on the server for a page language and a request's Accept-Language header (Sprint\n * 25.22's review), for server code's own dates and numbers: a server page passes it to the helpers from\n * `gridsmith-ui/utils`, so its words read as the components' beside them. `requestLocale(LANG, (await\n * headers()).get('accept-language'))`.\n */\nexport function requestLocale(lang: string | undefined, acceptLanguage?: string | null): string {\n return localeChoice(lang, acceptLanguage).server;\n}\n\n/** The most of a request's languages read: browsers send a handful. */\nconst MAX_LANGUAGES = 20;\n\n/**\n * A request's `Accept-Language` header as the languages a browser lists (Sprint 25.20 L1): highest quality first, ties in\n * the header's order, \"*\" and anything with q=0 left out. \"en-GB,en;q=0.9,fr;q=0.8\" is [\"en-GB\", \"en\", \"fr\"].\n */\nexport function acceptedLanguages(header: string | null | undefined): string[] {\n if (!header) return [];\n const read: Array<{ tag: string; q: number; at: number }> = [];\n // a browser lists a handful; a header made up to be long reads no further than the first twenty (the review)\n header.split(\",\").slice(0, MAX_LANGUAGES).forEach((part, at) => {\n const [tag, ...params] = part.trim().split(\";\");\n const name = tag?.trim();\n if (!name || name === \"*\") return;\n const qParam = params.map((p) => p.trim()).find((p) => /^q=/i.test(p));\n const q = qParam ? Number(qParam.slice(2)) : 1;\n if (!(q > 0)) return;\n read.push({ tag: name, q, at });\n });\n return read.sort((a, b) => b.q - a.q || a.at - b.at).map((r) => r.tag);\n}\n\n/** The language and script a tag is written in: \"en\" and \"en-GB\" are both \"en-Latn\"; \"zh-TW\" is \"zh-Hant\", \"zh-CN\" \"zh-Hans\". */\nfunction writtenIn(tag: string): string | null {\n try {\n const locale = new Intl.Locale(tag).maximize();\n return `${locale.language}-${locale.script ?? \"\"}`;\n } catch {\n return null;\n }\n}\n\n/**\n * A visitor's tag as its language, script and region (Sprint 25.20's review): a request's header can carry private-use,\n * extension and variant parts (\"en-x-…\", \"en-GB-u-nu-thai\") that would change the digits a server writes and make every\n * cache keyed by locale grow with each new header. Read by syntax alone, so a server and a browser reduce a tag alike.\n */\nfunction plainTag(tag: string): string | undefined {\n try {\n const locale = new Intl.Locale(tag);\n return [locale.language, locale.script, locale.region].filter(Boolean).join(\"-\");\n } catch {\n return undefined;\n }\n}\n\nfunction choose(page: string, spoken: readonly string[]): string | undefined {\n const language = writtenIn(page);\n // a tag Intl can't read: the browser's language\n if (language === null) return undefined;\n // the visitor's own form of the page's language (\"en-GB\" for an \"en\" page), wherever it is in their list; else the page's\n const own = spoken.find((tag) => typeof tag === \"string\" && writtenIn(tag) === language);\n try {\n return Intl.getCanonicalLocales((own && plainTag(own)) || page)[0];\n } catch {\n return undefined;\n }\n}\n"]}
@@ -1,8 +1,7 @@
1
1
  "use client";
2
- import { useRevealFocus } from './chunk-HTXP5BMM.js';
3
- import { tabStops } from './chunk-ZI3OQDET.js';
4
- import { createContext, useContext, useState, useLayoutEffect, useEffect } from 'react';
2
+ import { createContext, useContext, useState, useLayoutEffect } from 'react';
5
3
 
4
+ // ../../showcase/src/lib/use-fill-height.ts
6
5
  function useFillHeight(ref, enabled = true) {
7
6
  const insideFill = useContext(FillContext);
8
7
  const active = enabled && !insideFill;
@@ -128,50 +127,7 @@ function spaceBelow(el, main) {
128
127
  }
129
128
  return space;
130
129
  }
131
- function useKeyboardScroll(el, label, enabled = true, nameFromHeading = false) {
132
- useRevealFocus(el, enabled);
133
- useEffect(() => {
134
- if (!enabled || !el || typeof ResizeObserver === "undefined") return;
135
- const name = () => {
136
- const heading = nameFromHeading ? el.querySelector("h1, h2, h3, h4, h5, h6")?.textContent?.trim() : "";
137
- return heading || label;
138
- };
139
- const set = (on) => {
140
- if (on === el.hasAttribute("data-scroll-stop")) {
141
- if (on && el.getAttribute("aria-label") !== name()) el.setAttribute("aria-label", name());
142
- return;
143
- }
144
- if (on) {
145
- el.setAttribute("data-scroll-stop", "");
146
- el.tabIndex = 0;
147
- el.setAttribute("role", "region");
148
- el.setAttribute("aria-label", name());
149
- } else {
150
- el.removeAttribute("data-scroll-stop");
151
- el.removeAttribute("tabindex");
152
- el.removeAttribute("role");
153
- el.removeAttribute("aria-label");
154
- }
155
- };
156
- const update = () => {
157
- const scrolls = el.scrollHeight > el.clientHeight + 1 && tabStops(el).length === 0;
158
- set(scrolls || el.hasAttribute("data-scroll-stop") && document.activeElement === el);
159
- };
160
- update();
161
- const ro = new ResizeObserver(update);
162
- ro.observe(el);
163
- const mo = typeof MutationObserver === "undefined" ? null : new MutationObserver(update);
164
- mo?.observe(el, { childList: true, subtree: true, characterData: true });
165
- el.addEventListener("focusout", update);
166
- return () => {
167
- ro.disconnect();
168
- mo?.disconnect();
169
- el.removeEventListener("focusout", update);
170
- set(false);
171
- };
172
- }, [el, label, enabled, nameFromHeading]);
173
- }
174
130
 
175
- export { FillContext, useFillHeight, useKeyboardScroll };
176
- //# sourceMappingURL=chunk-FD4YED7O.js.map
177
- //# sourceMappingURL=chunk-FD4YED7O.js.map
131
+ export { FillContext, useFillHeight };
132
+ //# sourceMappingURL=chunk-DBHVJ7Z3.js.map
133
+ //# sourceMappingURL=chunk-DBHVJ7Z3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../showcase/src/lib/use-fill-height.ts"],"names":[],"mappings":"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","file":"chunk-DBHVJ7Z3.js","sourcesContent":["import { createContext, useContext, useLayoutEffect, useState, type RefObject } from \"react\";\n\n/**\n * Sprint 25.16 A42 (P31): a page led by a chart, a board or a split view fills the height under its header, and its\n * content scrolls inside it; the page doesn't. SplitView (frame=\"page\"), Gantt and KanbanBoard use this one mechanism.\n *\n * The element takes the rest of the shell's <main> height (main's height less what sits above it and the padding below\n * it) when all of these hold:\n * - it leads the page: above it there is only the page's header, its chrome (notices, tabs, a toolbar, a search or\n * filters row, however tall) and short rows, so a board in a dashboard's last card keeps its own height;\n * - it is the page's last content: nothing after it, up to <main>, takes room, so nothing below it is cut off (the\n * shell's footer is not the page's: it sits below the fold, as it does under a SplitView frame=\"fill\");\n * - its height follows its content: neither its own class nor a box around it gives it one (a fixed height, a flex\n * column that already gives it the rest), and given more, the page grows;\n * - the rest is tall enough to use (a phone on its side keeps the natural height, and the page scrolls).\n * Outside the shell's <main> (AppShell marks it), or inside a view that fills (a Gantt in a SplitView's pane), it keeps\n * its own height. Returns the height in pixels, or null for the natural height.\n */\nexport function useFillHeight(ref: RefObject<HTMLElement | null>, enabled = true): number | null {\n const insideFill = useContext(FillContext);\n const active = enabled && !insideFill;\n const [height, setHeight] = useState<number | null>(null);\n useLayoutEffect(() => {\n const main = ref.current?.closest<HTMLElement>(\"main[data-shell-main]\");\n if (!active || !main || typeof ResizeObserver === \"undefined\") {\n setHeight(null);\n return;\n }\n let frame = 0;\n // the element is read on each measure: a view that changes its layout (stacked or side by side) changes its root\n const measure = () => {\n frame = 0;\n const el = ref.current;\n setHeight(el && main.contains(el) ? fillHeight(el, main) : null);\n };\n const schedule = () => {\n if (!frame) frame = requestAnimationFrame(measure);\n };\n measure();\n // main's height (the window, a phone's toolbar), and what sits above or after the element at the page's top level (a\n // header's actions wrapping, a banner or a note arriving): the boxes there now, and any that arrive\n const ro = new ResizeObserver(schedule);\n const watch = (box: Element) => {\n ro.observe(box);\n for (const child of Array.from(box.children)) ro.observe(child);\n };\n const page = main.querySelector<HTMLElement>(\"[data-shell-content]\");\n watch(main);\n if (page) watch(page);\n const mo = typeof MutationObserver === \"undefined\" ? null : new MutationObserver((records) => {\n for (const record of records) record.addedNodes.forEach((node) => { if (node instanceof Element) ro.observe(node); });\n schedule();\n });\n mo?.observe(main, { childList: true });\n if (page) mo?.observe(page, { childList: true });\n return () => {\n ro.disconnect();\n mo?.disconnect();\n if (frame) cancelAnimationFrame(frame);\n };\n }, [ref, active]);\n return active ? height : null;\n}\n\n/** Content inside a view that fills: it takes the view's height (h-full), never the page's. */\nexport const FillContext = createContext(false);\n\n/** Below this the rest of the screen is too short to scroll inside (about six rows of a chart). */\nconst MIN_FILL = 240;\n/** A row that may sit between the header and the view: tabs, a toolbar, a filters row (PageContainer's ROW_MAX). */\nconst ROW_MAX = 56;\n\nfunction fillHeight(el: HTMLElement, main: HTMLElement): number | null {\n // the page's own content: the shell's wrapper above its footer, else main\n const page = el.closest<HTMLElement>(\"[data-shell-content]\") ?? main;\n if (!leadsPage(el, page) || !isLastContent(el, page) || keepScroll(el, () => hasOwnHeight(el) || !followsContent(el, main))) return null;\n const mainBox = main.getBoundingClientRect();\n const top = el.getBoundingClientRect().top - mainBox.top - main.clientTop + main.scrollTop;\n const height = Math.floor(main.clientHeight - top - spaceBelow(el, main));\n return height >= MIN_FILL ? height : null;\n}\n\n/**\n * Runs a probe that changes the element's height for a moment, and puts back where everything inside it was scrolled:\n * at its natural height a region that scrolls inside it stops scrolling, and the browser drops its position (Sprint\n * 25.18: the week's hours opened at the working day, then the next measure sent them back to midnight).\n */\nfunction keepScroll<T>(el: HTMLElement, probe: () => T): T {\n const scrolled: Array<[HTMLElement, number, number]> = [];\n for (const node of [el, ...Array.from(el.querySelectorAll<HTMLElement>(\"*\"))]) {\n if (node.scrollTop || node.scrollLeft) scrolled.push([node, node.scrollTop, node.scrollLeft]);\n }\n try {\n return probe();\n } finally {\n for (const [node, top, left] of scrolled) {\n node.scrollTop = top;\n node.scrollLeft = left;\n }\n }\n}\n\n/** Whether a box takes room on the page: an overlay, a portal's anchor, a hidden or an empty node doesn't. */\nfunction takesRoom(box: Element): boolean {\n const style = getComputedStyle(box);\n if (style.display === \"none\" || style.position === \"absolute\" || style.position === \"fixed\") return false;\n const rect = box.getBoundingClientRect();\n return rect.height > 0 && rect.width > 0;\n}\n\n/**\n * The page's chrome, at any height: a notice (an Alert, a banner that wraps to two lines on a phone), tabs, a toolbar, a\n * search or filters row (the review of 1 October: a two-line banner turned the fill off). Content of the page's own (a\n * row of stats, cards, text) is not chrome.\n */\nconst CHROME = '[role=\"alert\"], [role=\"status\"], [role=\"tablist\"], [role=\"toolbar\"], [role=\"search\"]';\nconst isChrome = (box: Element) =>\n box.matches(CHROME) || (box.children.length > 0 && Array.from(box.children).every((child) => !takesRoom(child) || child.matches(CHROME)));\n\n/** Above the element, up to the page's root: only the page's header (its title), its chrome, and rows no taller than ROW_MAX. */\nfunction leadsPage(el: HTMLElement, page: HTMLElement): boolean {\n for (let node: HTMLElement | null = el; node && node !== page; node = node.parentElement) {\n for (let prev = node.previousElementSibling; prev; prev = prev.previousElementSibling) {\n if (!takesRoom(prev)) continue;\n if (prev.matches('[data-gs=\"page-header\"]') || prev.querySelector('[data-gs=\"page-header\"], h1')) continue;\n if (isChrome(prev)) continue;\n if (prev.getBoundingClientRect().height > ROW_MAX) return false;\n }\n }\n return true;\n}\n\n/** Nothing after the element, up to the page's root, takes room. */\nfunction isLastContent(el: HTMLElement, page: HTMLElement): boolean {\n for (let node: HTMLElement | null = el; node && node !== page; node = node.parentElement) {\n for (let next = node.nextElementSibling; next; next = next.nextElementSibling) {\n if (takesRoom(next)) return false;\n }\n }\n return true;\n}\n\n/**\n * The element's own CSS gives it a height that isn't its content's: a fixed height of its own (`h-[420px]`), or `h-full`\n * that resolves against a box which already has one. It keeps it. Read with the fill's own height set aside.\n */\nfunction hasOwnHeight(el: HTMLElement): boolean {\n const inline = el.style.height;\n el.style.height = \"\";\n const own = el.offsetHeight;\n el.style.height = \"auto\";\n const natural = el.offsetHeight;\n el.style.height = inline;\n return Math.abs(own - natural) > 1;\n}\n\n/**\n * Given more height, does the page grow? Probed once: the element is made a screen taller and main's top-level box\n * measured. A box of fixed height around it, or a flex column that already gives it its height, absorbs it.\n */\nfunction followsContent(el: HTMLElement, main: HTMLElement): boolean {\n let top: HTMLElement = el;\n while (top.parentElement && top.parentElement !== main) top = top.parentElement;\n const before = top.offsetHeight;\n const inline = el.style.height;\n el.style.height = `${el.offsetHeight + main.clientHeight}px`;\n const grows = top.offsetHeight > before;\n el.style.height = inline;\n return grows;\n}\n\n/** The padding, borders and margins under the element, up to main's own padding: they stay below it. */\nfunction spaceBelow(el: HTMLElement, main: HTMLElement): number {\n const px = (value: string) => parseFloat(value) || 0;\n let space = px(getComputedStyle(main).paddingBottom);\n for (let node: HTMLElement | null = el; node && node !== main; node = node.parentElement) {\n const style = getComputedStyle(node);\n space += px(style.marginBottom);\n if (node !== el) space += px(style.paddingBottom) + px(style.borderBottomWidth);\n }\n return space;\n}\n"]}
@@ -0,0 +1,110 @@
1
+ "use client";
2
+ import { useLatest } from './chunk-WIUGPZOH.js';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
+
5
+ var registry = /* @__PURE__ */ new WeakMap();
6
+ var sharedObserver = null;
7
+ var rafId = null;
8
+ var pendingElements = /* @__PURE__ */ new Set();
9
+ function getSharedObserver() {
10
+ if (sharedObserver) return sharedObserver;
11
+ sharedObserver = new ResizeObserver((entries) => {
12
+ for (const entry of entries) {
13
+ pendingElements.add(entry.target);
14
+ }
15
+ if (rafId === null) {
16
+ rafId = requestAnimationFrame(flush);
17
+ }
18
+ });
19
+ return sharedObserver;
20
+ }
21
+ function flush() {
22
+ rafId = null;
23
+ for (const el of pendingElements) {
24
+ const callbacks = registry.get(el);
25
+ if (callbacks) {
26
+ for (const cb of callbacks) cb();
27
+ }
28
+ }
29
+ pendingElements.clear();
30
+ }
31
+ function observe(el, callback) {
32
+ let set = registry.get(el);
33
+ if (!set) {
34
+ set = /* @__PURE__ */ new Set();
35
+ registry.set(el, set);
36
+ }
37
+ set.add(callback);
38
+ if (set.size === 1) {
39
+ getSharedObserver().observe(el);
40
+ }
41
+ }
42
+ function unobserve(el, callback) {
43
+ const set = registry.get(el);
44
+ if (!set) return;
45
+ set.delete(callback);
46
+ if (set.size === 0) {
47
+ registry.delete(el);
48
+ getSharedObserver().unobserve(el);
49
+ }
50
+ }
51
+ var TOLERANCE = 1;
52
+ function checkOverflow(el, lines) {
53
+ if (lines <= 1) {
54
+ return el.scrollWidth - el.clientWidth > TOLERANCE;
55
+ }
56
+ return el.scrollHeight - el.clientHeight > TOLERANCE;
57
+ }
58
+ function useTextOverflow(options = {}) {
59
+ const { lines = 1, disabled = false, content } = options;
60
+ const [isTruncated, setIsTruncated] = useState(false);
61
+ const elementRef = useRef(null);
62
+ const registeredRef = useRef(null);
63
+ const doCheck = useCallback(() => {
64
+ if (!elementRef.current || disabled) {
65
+ setIsTruncated(false);
66
+ return;
67
+ }
68
+ const next = checkOverflow(elementRef.current, lines);
69
+ setIsTruncated((prev) => prev !== next ? next : prev);
70
+ }, [lines, disabled]);
71
+ const checkRef = useLatest(doCheck);
72
+ useEffect(() => {
73
+ if (disabled) {
74
+ setIsTruncated(false);
75
+ } else if (elementRef.current) {
76
+ const next = checkOverflow(elementRef.current, lines);
77
+ setIsTruncated((prev) => prev !== next ? next : prev);
78
+ }
79
+ }, [content, lines, disabled]);
80
+ const ref = useCallback(
81
+ (node) => {
82
+ const prev = elementRef.current;
83
+ if (prev && registeredRef.current) {
84
+ unobserve(prev, registeredRef.current);
85
+ registeredRef.current = null;
86
+ }
87
+ elementRef.current = node;
88
+ if (node && !disabled) {
89
+ const wrapper = () => checkRef.current?.();
90
+ registeredRef.current = wrapper;
91
+ observe(node, wrapper);
92
+ const initial = checkOverflow(node, lines);
93
+ setIsTruncated(initial);
94
+ }
95
+ },
96
+ [lines, disabled, checkRef]
97
+ );
98
+ useEffect(() => {
99
+ return () => {
100
+ if (elementRef.current && registeredRef.current) {
101
+ unobserve(elementRef.current, registeredRef.current);
102
+ }
103
+ };
104
+ }, []);
105
+ return { ref, isTruncated };
106
+ }
107
+
108
+ export { useTextOverflow };
109
+ //# sourceMappingURL=chunk-IELIF4PJ.js.map
110
+ //# sourceMappingURL=chunk-IELIF4PJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../showcase/src/lib/use-text-overflow.ts"],"names":[],"mappings":"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","file":"chunk-IELIF4PJ.js","sourcesContent":["import { useState, useCallback, useRef, useEffect } from \"react\";\nimport { useLatest } from \"./use-latest\";\n\n// ─── Shared ResizeObserver (singleton per document) ────────────────────────────\n// All useTextOverflow instances share a single ResizeObserver. This avoids the\n// performance cost of 80+ independent observers on pages with many truncated elements.\n\ntype OverflowCallback = () => void;\n\nconst registry = new WeakMap<Element, Set<OverflowCallback>>();\nlet sharedObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nconst pendingElements = new Set<Element>();\n\nfunction getSharedObserver(): ResizeObserver {\n if (sharedObserver) return sharedObserver;\n sharedObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n pendingElements.add(entry.target);\n }\n if (rafId === null) {\n rafId = requestAnimationFrame(flush);\n }\n });\n return sharedObserver;\n}\n\nfunction flush() {\n rafId = null;\n for (const el of pendingElements) {\n const callbacks = registry.get(el);\n if (callbacks) {\n for (const cb of callbacks) cb();\n }\n }\n pendingElements.clear();\n}\n\nfunction observe(el: Element, callback: OverflowCallback) {\n let set = registry.get(el);\n if (!set) {\n set = new Set();\n registry.set(el, set);\n }\n set.add(callback);\n if (set.size === 1) {\n getSharedObserver().observe(el);\n }\n}\n\nfunction unobserve(el: Element, callback: OverflowCallback) {\n const set = registry.get(el);\n if (!set) return;\n set.delete(callback);\n if (set.size === 0) {\n registry.delete(el);\n getSharedObserver().unobserve(el);\n }\n}\n\n// ─── Overflow detection ────────────────────────────────────────────────────────\n\nconst TOLERANCE = 1; // px — accounts for integer rounding in scrollWidth/clientWidth\n\nfunction checkOverflow(el: HTMLElement, lines: number): boolean {\n if (lines <= 1) {\n return el.scrollWidth - el.clientWidth > TOLERANCE;\n }\n return el.scrollHeight - el.clientHeight > TOLERANCE;\n}\n\n// ─── Hook ──────────────────────────────────────────────────────────────────────\n\nexport interface UseTextOverflowOptions {\n /** Number of visible lines before truncation. 1 = single-line ellipsis, >1 = line-clamp. */\n lines?: number;\n /** Disable overflow detection (e.g. when component is hidden) */\n disabled?: boolean;\n /** Content key — triggers re-check when this value changes (use for dynamic text content) */\n content?: unknown;\n}\n\nexport interface UseTextOverflowResult {\n /** Callback ref — attach to the element that has truncation CSS applied */\n ref: (node: HTMLElement | null) => void;\n /** True when the element's text is currently overflowing/truncated */\n isTruncated: boolean;\n}\n\n/**\n * Detects whether an element's text content is overflowing (truncated).\n * Uses a single shared ResizeObserver for all instances on the page.\n *\n * @example\n * const { ref, isTruncated } = useTextOverflow({ lines: 1 });\n * return (\n * <Tooltip content={fullText} disabled={!isTruncated}>\n * <span ref={ref} className=\"truncate\">{fullText}</span>\n * </Tooltip>\n * );\n */\nexport function useTextOverflow(options: UseTextOverflowOptions = {}): UseTextOverflowResult {\n const { lines = 1, disabled = false, content } = options;\n const [isTruncated, setIsTruncated] = useState(false);\n const elementRef = useRef<HTMLElement | null>(null);\n // The observer-registered wrapper delegates to the latest check logic via checkRef.\n const registeredRef = useRef<OverflowCallback | null>(null);\n\n const doCheck = useCallback(() => {\n if (!elementRef.current || disabled) {\n setIsTruncated(false);\n return;\n }\n const next = checkOverflow(elementRef.current, lines);\n setIsTruncated((prev) => (prev !== next ? next : prev));\n }, [lines, disabled]);\n\n // Latest check logic, updated in a layout effect so the stable wrapper never runs a stale closure.\n const checkRef = useLatest<OverflowCallback>(doCheck);\n\n // Re-check when content changes (handles dynamic text like Dropdown selection)\n // or when disabled state changes\n // Measures the committed DOM, so it must stay a post-layout effect.\n /* eslint-disable react-hooks/set-state-in-effect -- overflow is a layout measurement */\n useEffect(() => {\n if (disabled) {\n setIsTruncated(false);\n } else if (elementRef.current) {\n const next = checkOverflow(elementRef.current, lines);\n setIsTruncated((prev) => (prev !== next ? next : prev));\n }\n }, [content, lines, disabled]);\n /* eslint-enable react-hooks/set-state-in-effect */\n\n const ref = useCallback(\n (node: HTMLElement | null) => {\n const prev = elementRef.current;\n\n // Unregister previous element using the SAME function we registered with\n if (prev && registeredRef.current) {\n unobserve(prev, registeredRef.current);\n registeredRef.current = null;\n }\n\n elementRef.current = node;\n\n if (node && !disabled) {\n // Stable wrapper that delegates to the latest check logic\n const wrapper: OverflowCallback = () => checkRef.current?.();\n registeredRef.current = wrapper;\n observe(node, wrapper);\n // Initial check on mount\n const initial = checkOverflow(node, lines);\n setIsTruncated(initial);\n }\n },\n [lines, disabled, checkRef],\n );\n\n // Cleanup on unmount — uses the registered wrapper, not the check function\n useEffect(() => {\n return () => {\n if (elementRef.current && registeredRef.current) {\n unobserve(elementRef.current, registeredRef.current);\n }\n };\n }, []);\n\n return { ref, isTruncated };\n}\n"]}