@proteos/ui 0.61.0 → 0.76.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 (43) hide show
  1. package/dist/{chunk-NWGG5WFL.cjs → chunk-4ACELYEP.cjs} +23 -4
  2. package/dist/chunk-4ACELYEP.cjs.map +1 -0
  3. package/dist/{chunk-NSE4VW3Z.js → chunk-XRN5MNDX.js} +24 -5
  4. package/dist/chunk-XRN5MNDX.js.map +1 -0
  5. package/dist/index.cjs +811 -304
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +365 -15
  8. package/dist/index.d.ts +365 -15
  9. package/dist/index.js +537 -39
  10. package/dist/index.js.map +1 -1
  11. package/dist/{pdf-view-RRMAUK3L.cjs → pdf-view-2DSARHNQ.cjs} +7 -7
  12. package/dist/{pdf-view-RRMAUK3L.cjs.map → pdf-view-2DSARHNQ.cjs.map} +1 -1
  13. package/dist/{pdf-view-RQ2OZYHJ.js → pdf-view-2QU4FI37.js} +3 -3
  14. package/dist/{pdf-view-RQ2OZYHJ.js.map → pdf-view-2QU4FI37.js.map} +1 -1
  15. package/package.json +2 -2
  16. package/src/components/availability-calendar/availability-calendar.stories.tsx +78 -0
  17. package/src/components/availability-calendar/availability-calendar.test.tsx +89 -0
  18. package/src/components/availability-calendar/availability-calendar.tsx +182 -0
  19. package/src/components/availability-calendar/index.ts +5 -0
  20. package/src/components/button/button.stories.tsx +22 -1
  21. package/src/components/button/button.test.tsx +45 -1
  22. package/src/components/button/button.tsx +32 -2
  23. package/src/components/conversation-brief-card/conversation-brief-preview.tsx +176 -47
  24. package/src/components/conversation-brief-card/conversation-brief-review.test.tsx +58 -1
  25. package/src/components/data-table/data-table.test.tsx +65 -0
  26. package/src/components/data-table/data-table.tsx +33 -2
  27. package/src/components/glow-frame/glow-frame.stories.tsx +65 -0
  28. package/src/components/glow-frame/glow-frame.test.tsx +67 -0
  29. package/src/components/glow-frame/glow-frame.tsx +182 -0
  30. package/src/components/glow-frame/index.ts +2 -0
  31. package/src/components/stepper/index.ts +8 -0
  32. package/src/components/stepper/stepper.stories.tsx +84 -0
  33. package/src/components/stepper/stepper.test.tsx +79 -0
  34. package/src/components/stepper/stepper.tsx +127 -0
  35. package/src/components/time-slot-list/index.ts +2 -0
  36. package/src/components/time-slot-list/time-slot-list.stories.tsx +100 -0
  37. package/src/components/time-slot-list/time-slot-list.test.tsx +120 -0
  38. package/src/components/time-slot-list/time-slot-list.tsx +154 -0
  39. package/src/components/tooltip/tooltip.tsx +4 -1
  40. package/src/index.ts +4 -0
  41. package/src/styles.css +216 -0
  42. package/dist/chunk-NSE4VW3Z.js.map +0 -1
  43. package/dist/chunk-NWGG5WFL.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNWGG5WFL_cjs = require('./chunk-NWGG5WFL.cjs');
3
+ var chunk4ACELYEP_cjs = require('./chunk-4ACELYEP.cjs');
4
4
  var lucideReact = require('lucide-react');
5
5
  var react = require('react');
6
6
  var reactPdf = require('react-pdf');
@@ -39,7 +39,7 @@ function PdfView({ name, src, onDownload }) {
39
39
  setPageNumber(1);
40
40
  },
41
41
  onLoadError: () => setLoadError(true),
42
- loading: /* @__PURE__ */ jsxRuntime.jsx(chunkNWGG5WFL_cjs.Skeleton, { "aria-label": `Loading ${name}`, className: "h-full w-full rounded-none" }),
42
+ loading: /* @__PURE__ */ jsxRuntime.jsx(chunk4ACELYEP_cjs.Skeleton, { "aria-label": `Loading ${name}`, className: "h-full w-full rounded-none" }),
43
43
  error: null,
44
44
  children: pageWidth ? /* @__PURE__ */ jsxRuntime.jsx(
45
45
  reactPdf.Page,
@@ -54,7 +54,7 @@ function PdfView({ name, src, onDownload }) {
54
54
  ) }),
55
55
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center justify-center gap-1.5 border-t border-rule bg-surface px-2 py-1", children: [
56
56
  /* @__PURE__ */ jsxRuntime.jsx(
57
- chunkNWGG5WFL_cjs.Button,
57
+ chunk4ACELYEP_cjs.Button,
58
58
  {
59
59
  variant: "ghost",
60
60
  size: "icon-sm",
@@ -66,7 +66,7 @@ function PdfView({ name, src, onDownload }) {
66
66
  ),
67
67
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-14 text-center font-sans text-[12px] tabular-nums text-ink-3", children: pageCount ? `${pageNumber} / ${pageCount}` : "\u2013" }),
68
68
  /* @__PURE__ */ jsxRuntime.jsx(
69
- chunkNWGG5WFL_cjs.Button,
69
+ chunk4ACELYEP_cjs.Button,
70
70
  {
71
71
  variant: "ghost",
72
72
  size: "icon-sm",
@@ -77,7 +77,7 @@ function PdfView({ name, src, onDownload }) {
77
77
  }
78
78
  ),
79
79
  onDownload ? /* @__PURE__ */ jsxRuntime.jsx(
80
- chunkNWGG5WFL_cjs.Button,
80
+ chunk4ACELYEP_cjs.Button,
81
81
  {
82
82
  variant: "ghost",
83
83
  size: "icon-sm",
@@ -91,5 +91,5 @@ function PdfView({ name, src, onDownload }) {
91
91
  }
92
92
 
93
93
  module.exports = PdfView;
94
- //# sourceMappingURL=pdf-view-RRMAUK3L.cjs.map
95
- //# sourceMappingURL=pdf-view-RRMAUK3L.cjs.map
94
+ //# sourceMappingURL=pdf-view-2DSARHNQ.cjs.map
95
+ //# sourceMappingURL=pdf-view-2DSARHNQ.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["url-asset:pdfjs-dist/build/pdf.worker.min.mjs?url","../src/components/file-viewer/pdf-view.tsx"],"names":["pdfjs","useState","useRef","useEffect","jsx","jsxs","Document","Skeleton","Page","Button","ChevronLeft","ChevronRight","Download"],"mappings":"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","file":"pdf-view-RRMAUK3L.cjs","sourcesContent":["export default \"https://unpkg.com/pdfjs-dist@5.4.296/build/pdf.worker.min.mjs\"","import { ChevronLeft, ChevronRight, Download } from 'lucide-react'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\nimport { useEffect, useRef, useState } from 'react'\nimport { Document, Page, pdfjs } from 'react-pdf'\n\nimport { Button } from '../button/button'\nimport { Skeleton } from '../skeleton/skeleton'\n\n/* =========================================================================\n PdfView — the FileViewer's PDF pane, backed by react-pdf (pdf.js).\n Lazy-loaded by the FileViewer (default export) so pdf.js and its worker\n stay out of the main bundle. Text/annotation layers are off: this is a\n read-only preview and the layers would need pdf.js's own stylesheets,\n which the token-based design system deliberately doesn't import.\n ========================================================================= */\n\n// The worker must come from the SAME pdfjs-dist instance react-pdf renders\n// with (API/worker version mismatch throws). `?url` lets Vite emit it as an\n// asset and hand back the served URL. In the published dist build the `?url`\n// module is rewritten to a version-pinned CDN URL (see tsup.config.ts).\n// react-pdf's entry unconditionally stomps workerSrc with the placeholder\n// 'pdf.worker.mjs' when it's imported (which happens right above), so that\n// sentinel counts as unset — only a consumer assignment made after react-pdf\n// loaded can stick.\nif (\n !pdfjs.GlobalWorkerOptions.workerSrc ||\n pdfjs.GlobalWorkerOptions.workerSrc === 'pdf.worker.mjs'\n) {\n pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n}\n\nexport interface PdfViewProps {\n name: string\n /** Blob object URL of the PDF bytes. */\n src: string\n onDownload?: () => void\n}\n\nexport default function PdfView({ name, src, onDownload }: PdfViewProps) {\n const [pageCount, setPageCount] = useState<number | null>(null)\n const [pageNumber, setPageNumber] = useState(1)\n const [loadError, setLoadError] = useState(false)\n\n // Fit-width: render the page at the scroll area's current width.\n const scrollRef = useRef<HTMLDivElement | null>(null)\n const [pageWidth, setPageWidth] = useState<number | null>(null)\n useEffect(() => {\n const element = scrollRef.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setPageWidth(Math.floor(entry.contentRect.width))\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [])\n\n if (loadError) {\n return (\n <div className=\"flex h-full w-full items-center justify-center p-4\">\n <span className=\"font-sans text-[13px] text-danger\">\n Could not display this PDF. Download it instead.\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"flex h-full w-full flex-col\">\n <div ref={scrollRef} className=\"min-h-0 flex-1 overflow-auto\">\n <Document\n file={src}\n onLoadSuccess={({ numPages }) => {\n setPageCount(numPages)\n setPageNumber(1)\n }}\n onLoadError={() => setLoadError(true)}\n loading={\n <Skeleton aria-label={`Loading ${name}`} className=\"h-full w-full rounded-none\" />\n }\n error={null}\n >\n {pageWidth ? (\n <Page\n pageNumber={pageNumber}\n width={pageWidth}\n renderTextLayer={false}\n renderAnnotationLayer={false}\n />\n ) : null}\n </Document>\n </div>\n <div className=\"flex shrink-0 items-center justify-center gap-1.5 border-t border-rule bg-surface px-2 py-1\">\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Previous page\"\n isDisabled={pageNumber <= 1}\n onPress={() => setPageNumber((page) => Math.max(1, page - 1))}\n >\n <ChevronLeft aria-hidden />\n </Button>\n <span className=\"min-w-14 text-center font-sans text-[12px] tabular-nums text-ink-3\">\n {pageCount ? `${pageNumber} / ${pageCount}` : '–'}\n </span>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Next page\"\n isDisabled={pageCount === null || pageNumber >= pageCount}\n onPress={() => setPageNumber((page) => Math.min(pageCount ?? page, page + 1))}\n >\n <ChevronRight aria-hidden />\n </Button>\n {onDownload ? (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Download ${name}`}\n onPress={onDownload}\n >\n <Download aria-hidden />\n </Button>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"sources":["url-asset:pdfjs-dist/build/pdf.worker.min.mjs?url","../src/components/file-viewer/pdf-view.tsx"],"names":["pdfjs","useState","useRef","useEffect","jsx","jsxs","Document","Skeleton","Page","Button","ChevronLeft","ChevronRight","Download"],"mappings":";;;;;;;;;AAAA,IAAO,sBAAA,GAAQ,+DAAA;ACwBf,IACE,CAACA,cAAA,CAAM,mBAAA,CAAoB,aAC3BA,cAAA,CAAM,mBAAA,CAAoB,cAAc,gBAAA,EACxC;AACA,EAAAA,cAAA,CAAM,oBAAoB,SAAA,GAAY,sBAAA;AACxC;AASe,SAAR,OAAA,CAAyB,EAAE,IAAA,EAAM,GAAA,EAAK,YAAW,EAAiB;AACvE,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIC,eAAwB,IAAI,CAAA;AAC9D,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,eAAS,CAAC,CAAA;AAC9C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIA,eAAS,KAAK,CAAA;AAGhD,EAAA,MAAM,SAAA,GAAYC,aAA8B,IAAI,CAAA;AACpD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAID,eAAwB,IAAI,CAAA;AAC9D,EAAAE,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,UAAU,SAAA,CAAU,OAAA;AAC1B,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,MAAM,WAAW,IAAI,cAAA,CAAe,CAAC,CAAC,KAAK,CAAA,KAAM;AAC/C,MAAA,IAAI,OAAO,YAAA,CAAa,IAAA,CAAK,MAAM,KAAA,CAAM,WAAA,CAAY,KAAK,CAAC,CAAA;AAAA,IAC7D,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,QAAQ,OAAO,CAAA;AACxB,IAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AAAA,EACnC,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,uBACEC,cAAA,CAAC,SAAI,SAAA,EAAU,oDAAA,EACb,yCAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mCAAA,EAAoC,QAAA,EAAA,kDAAA,EAEpD,CAAA,EACF,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACEC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,6BAAA,EACb,QAAA,EAAA;AAAA,oBAAAD,cAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,SAAA,EAAW,SAAA,EAAU,8BAAA,EAC7B,QAAA,kBAAAA,cAAA;AAAA,MAACE,iBAAA;AAAA,MAAA;AAAA,QACC,IAAA,EAAM,GAAA;AAAA,QACN,aAAA,EAAe,CAAC,EAAE,QAAA,EAAS,KAAM;AAC/B,UAAA,YAAA,CAAa,QAAQ,CAAA;AACrB,UAAA,aAAA,CAAc,CAAC,CAAA;AAAA,QACjB,CAAA;AAAA,QACA,WAAA,EAAa,MAAM,YAAA,CAAa,IAAI,CAAA;AAAA,QACpC,OAAA,iCACGC,0BAAA,EAAA,EAAS,YAAA,EAAY,WAAW,IAAI,CAAA,CAAA,EAAI,WAAU,4BAAA,EAA6B,CAAA;AAAA,QAElF,KAAA,EAAO,IAAA;AAAA,QAEN,QAAA,EAAA,SAAA,mBACCH,cAAA;AAAA,UAACI,aAAA;AAAA,UAAA;AAAA,YACC,UAAA;AAAA,YACA,KAAA,EAAO,SAAA;AAAA,YACP,eAAA,EAAiB,KAAA;AAAA,YACjB,qBAAA,EAAuB;AAAA;AAAA,SACzB,GACE;AAAA;AAAA,KACN,EACF,CAAA;AAAA,oBACAH,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,6FAAA,EACb,QAAA,EAAA;AAAA,sBAAAD,cAAA;AAAA,QAACK,wBAAA;AAAA,QAAA;AAAA,UACC,OAAA,EAAQ,OAAA;AAAA,UACR,IAAA,EAAK,SAAA;AAAA,UACL,YAAA,EAAW,eAAA;AAAA,UACX,YAAY,UAAA,IAAc,CAAA;AAAA,UAC1B,OAAA,EAAS,MAAM,aAAA,CAAc,CAAC,IAAA,KAAS,KAAK,GAAA,CAAI,CAAA,EAAG,IAAA,GAAO,CAAC,CAAC,CAAA;AAAA,UAE5D,QAAA,kBAAAL,cAAA,CAACM,uBAAA,EAAA,EAAY,aAAA,EAAW,IAAA,EAAC;AAAA;AAAA,OAC3B;AAAA,sBACAN,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,oEAAA,EACb,QAAA,EAAA,SAAA,GAAY,GAAG,UAAU,CAAA,GAAA,EAAM,SAAS,CAAA,CAAA,GAAK,QAAA,EAChD,CAAA;AAAA,sBACAA,cAAA;AAAA,QAACK,wBAAA;AAAA,QAAA;AAAA,UACC,OAAA,EAAQ,OAAA;AAAA,UACR,IAAA,EAAK,SAAA;AAAA,UACL,YAAA,EAAW,WAAA;AAAA,UACX,UAAA,EAAY,SAAA,KAAc,IAAA,IAAQ,UAAA,IAAc,SAAA;AAAA,UAChD,OAAA,EAAS,MAAM,aAAA,CAAc,CAAC,IAAA,KAAS,IAAA,CAAK,GAAA,CAAI,SAAA,IAAa,IAAA,EAAM,IAAA,GAAO,CAAC,CAAC,CAAA;AAAA,UAE5E,QAAA,kBAAAL,cAAA,CAACO,wBAAA,EAAA,EAAa,aAAA,EAAW,IAAA,EAAC;AAAA;AAAA,OAC5B;AAAA,MACC,UAAA,mBACCP,cAAA;AAAA,QAACK,wBAAA;AAAA,QAAA;AAAA,UACC,OAAA,EAAQ,OAAA;AAAA,UACR,IAAA,EAAK,SAAA;AAAA,UACL,YAAA,EAAY,YAAY,IAAI,CAAA,CAAA;AAAA,UAC5B,OAAA,EAAS,UAAA;AAAA,UAET,QAAA,kBAAAL,cAAA,CAACQ,oBAAA,EAAA,EAAS,aAAA,EAAW,IAAA,EAAC;AAAA;AAAA,OACxB,GACE;AAAA,KAAA,EACN;AAAA,GAAA,EACF,CAAA;AAEJ","file":"pdf-view-2DSARHNQ.cjs","sourcesContent":["export default \"https://unpkg.com/pdfjs-dist@5.4.296/build/pdf.worker.min.mjs\"","import { ChevronLeft, ChevronRight, Download } from 'lucide-react'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\nimport { useEffect, useRef, useState } from 'react'\nimport { Document, Page, pdfjs } from 'react-pdf'\n\nimport { Button } from '../button/button'\nimport { Skeleton } from '../skeleton/skeleton'\n\n/* =========================================================================\n PdfView — the FileViewer's PDF pane, backed by react-pdf (pdf.js).\n Lazy-loaded by the FileViewer (default export) so pdf.js and its worker\n stay out of the main bundle. Text/annotation layers are off: this is a\n read-only preview and the layers would need pdf.js's own stylesheets,\n which the token-based design system deliberately doesn't import.\n ========================================================================= */\n\n// The worker must come from the SAME pdfjs-dist instance react-pdf renders\n// with (API/worker version mismatch throws). `?url` lets Vite emit it as an\n// asset and hand back the served URL. In the published dist build the `?url`\n// module is rewritten to a version-pinned CDN URL (see tsup.config.ts).\n// react-pdf's entry unconditionally stomps workerSrc with the placeholder\n// 'pdf.worker.mjs' when it's imported (which happens right above), so that\n// sentinel counts as unset — only a consumer assignment made after react-pdf\n// loaded can stick.\nif (\n !pdfjs.GlobalWorkerOptions.workerSrc ||\n pdfjs.GlobalWorkerOptions.workerSrc === 'pdf.worker.mjs'\n) {\n pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n}\n\nexport interface PdfViewProps {\n name: string\n /** Blob object URL of the PDF bytes. */\n src: string\n onDownload?: () => void\n}\n\nexport default function PdfView({ name, src, onDownload }: PdfViewProps) {\n const [pageCount, setPageCount] = useState<number | null>(null)\n const [pageNumber, setPageNumber] = useState(1)\n const [loadError, setLoadError] = useState(false)\n\n // Fit-width: render the page at the scroll area's current width.\n const scrollRef = useRef<HTMLDivElement | null>(null)\n const [pageWidth, setPageWidth] = useState<number | null>(null)\n useEffect(() => {\n const element = scrollRef.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setPageWidth(Math.floor(entry.contentRect.width))\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [])\n\n if (loadError) {\n return (\n <div className=\"flex h-full w-full items-center justify-center p-4\">\n <span className=\"font-sans text-[13px] text-danger\">\n Could not display this PDF. Download it instead.\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"flex h-full w-full flex-col\">\n <div ref={scrollRef} className=\"min-h-0 flex-1 overflow-auto\">\n <Document\n file={src}\n onLoadSuccess={({ numPages }) => {\n setPageCount(numPages)\n setPageNumber(1)\n }}\n onLoadError={() => setLoadError(true)}\n loading={\n <Skeleton aria-label={`Loading ${name}`} className=\"h-full w-full rounded-none\" />\n }\n error={null}\n >\n {pageWidth ? (\n <Page\n pageNumber={pageNumber}\n width={pageWidth}\n renderTextLayer={false}\n renderAnnotationLayer={false}\n />\n ) : null}\n </Document>\n </div>\n <div className=\"flex shrink-0 items-center justify-center gap-1.5 border-t border-rule bg-surface px-2 py-1\">\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Previous page\"\n isDisabled={pageNumber <= 1}\n onPress={() => setPageNumber((page) => Math.max(1, page - 1))}\n >\n <ChevronLeft aria-hidden />\n </Button>\n <span className=\"min-w-14 text-center font-sans text-[12px] tabular-nums text-ink-3\">\n {pageCount ? `${pageNumber} / ${pageCount}` : '–'}\n </span>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Next page\"\n isDisabled={pageCount === null || pageNumber >= pageCount}\n onPress={() => setPageNumber((page) => Math.min(pageCount ?? page, page + 1))}\n >\n <ChevronRight aria-hidden />\n </Button>\n {onDownload ? (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Download ${name}`}\n onPress={onDownload}\n >\n <Download aria-hidden />\n </Button>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Skeleton, Button } from './chunk-NSE4VW3Z.js';
1
+ import { Skeleton, Button } from './chunk-XRN5MNDX.js';
2
2
  import { ChevronLeft, ChevronRight, Download } from 'lucide-react';
3
3
  import { useState, useRef, useEffect } from 'react';
4
4
  import { pdfjs, Document, Page } from 'react-pdf';
@@ -89,5 +89,5 @@ function PdfView({ name, src, onDownload }) {
89
89
  }
90
90
 
91
91
  export { PdfView as default };
92
- //# sourceMappingURL=pdf-view-RQ2OZYHJ.js.map
93
- //# sourceMappingURL=pdf-view-RQ2OZYHJ.js.map
92
+ //# sourceMappingURL=pdf-view-2QU4FI37.js.map
93
+ //# sourceMappingURL=pdf-view-2QU4FI37.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["url-asset:pdfjs-dist/build/pdf.worker.min.mjs?url","../src/components/file-viewer/pdf-view.tsx"],"names":[],"mappings":"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","file":"pdf-view-RQ2OZYHJ.js","sourcesContent":["export default \"https://unpkg.com/pdfjs-dist@5.4.296/build/pdf.worker.min.mjs\"","import { ChevronLeft, ChevronRight, Download } from 'lucide-react'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\nimport { useEffect, useRef, useState } from 'react'\nimport { Document, Page, pdfjs } from 'react-pdf'\n\nimport { Button } from '../button/button'\nimport { Skeleton } from '../skeleton/skeleton'\n\n/* =========================================================================\n PdfView — the FileViewer's PDF pane, backed by react-pdf (pdf.js).\n Lazy-loaded by the FileViewer (default export) so pdf.js and its worker\n stay out of the main bundle. Text/annotation layers are off: this is a\n read-only preview and the layers would need pdf.js's own stylesheets,\n which the token-based design system deliberately doesn't import.\n ========================================================================= */\n\n// The worker must come from the SAME pdfjs-dist instance react-pdf renders\n// with (API/worker version mismatch throws). `?url` lets Vite emit it as an\n// asset and hand back the served URL. In the published dist build the `?url`\n// module is rewritten to a version-pinned CDN URL (see tsup.config.ts).\n// react-pdf's entry unconditionally stomps workerSrc with the placeholder\n// 'pdf.worker.mjs' when it's imported (which happens right above), so that\n// sentinel counts as unset — only a consumer assignment made after react-pdf\n// loaded can stick.\nif (\n !pdfjs.GlobalWorkerOptions.workerSrc ||\n pdfjs.GlobalWorkerOptions.workerSrc === 'pdf.worker.mjs'\n) {\n pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n}\n\nexport interface PdfViewProps {\n name: string\n /** Blob object URL of the PDF bytes. */\n src: string\n onDownload?: () => void\n}\n\nexport default function PdfView({ name, src, onDownload }: PdfViewProps) {\n const [pageCount, setPageCount] = useState<number | null>(null)\n const [pageNumber, setPageNumber] = useState(1)\n const [loadError, setLoadError] = useState(false)\n\n // Fit-width: render the page at the scroll area's current width.\n const scrollRef = useRef<HTMLDivElement | null>(null)\n const [pageWidth, setPageWidth] = useState<number | null>(null)\n useEffect(() => {\n const element = scrollRef.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setPageWidth(Math.floor(entry.contentRect.width))\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [])\n\n if (loadError) {\n return (\n <div className=\"flex h-full w-full items-center justify-center p-4\">\n <span className=\"font-sans text-[13px] text-danger\">\n Could not display this PDF. Download it instead.\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"flex h-full w-full flex-col\">\n <div ref={scrollRef} className=\"min-h-0 flex-1 overflow-auto\">\n <Document\n file={src}\n onLoadSuccess={({ numPages }) => {\n setPageCount(numPages)\n setPageNumber(1)\n }}\n onLoadError={() => setLoadError(true)}\n loading={\n <Skeleton aria-label={`Loading ${name}`} className=\"h-full w-full rounded-none\" />\n }\n error={null}\n >\n {pageWidth ? (\n <Page\n pageNumber={pageNumber}\n width={pageWidth}\n renderTextLayer={false}\n renderAnnotationLayer={false}\n />\n ) : null}\n </Document>\n </div>\n <div className=\"flex shrink-0 items-center justify-center gap-1.5 border-t border-rule bg-surface px-2 py-1\">\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Previous page\"\n isDisabled={pageNumber <= 1}\n onPress={() => setPageNumber((page) => Math.max(1, page - 1))}\n >\n <ChevronLeft aria-hidden />\n </Button>\n <span className=\"min-w-14 text-center font-sans text-[12px] tabular-nums text-ink-3\">\n {pageCount ? `${pageNumber} / ${pageCount}` : '–'}\n </span>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Next page\"\n isDisabled={pageCount === null || pageNumber >= pageCount}\n onPress={() => setPageNumber((page) => Math.min(pageCount ?? page, page + 1))}\n >\n <ChevronRight aria-hidden />\n </Button>\n {onDownload ? (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Download ${name}`}\n onPress={onDownload}\n >\n <Download aria-hidden />\n </Button>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"sources":["url-asset:pdfjs-dist/build/pdf.worker.min.mjs?url","../src/components/file-viewer/pdf-view.tsx"],"names":[],"mappings":"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","file":"pdf-view-2QU4FI37.js","sourcesContent":["export default \"https://unpkg.com/pdfjs-dist@5.4.296/build/pdf.worker.min.mjs\"","import { ChevronLeft, ChevronRight, Download } from 'lucide-react'\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\nimport { useEffect, useRef, useState } from 'react'\nimport { Document, Page, pdfjs } from 'react-pdf'\n\nimport { Button } from '../button/button'\nimport { Skeleton } from '../skeleton/skeleton'\n\n/* =========================================================================\n PdfView — the FileViewer's PDF pane, backed by react-pdf (pdf.js).\n Lazy-loaded by the FileViewer (default export) so pdf.js and its worker\n stay out of the main bundle. Text/annotation layers are off: this is a\n read-only preview and the layers would need pdf.js's own stylesheets,\n which the token-based design system deliberately doesn't import.\n ========================================================================= */\n\n// The worker must come from the SAME pdfjs-dist instance react-pdf renders\n// with (API/worker version mismatch throws). `?url` lets Vite emit it as an\n// asset and hand back the served URL. In the published dist build the `?url`\n// module is rewritten to a version-pinned CDN URL (see tsup.config.ts).\n// react-pdf's entry unconditionally stomps workerSrc with the placeholder\n// 'pdf.worker.mjs' when it's imported (which happens right above), so that\n// sentinel counts as unset — only a consumer assignment made after react-pdf\n// loaded can stick.\nif (\n !pdfjs.GlobalWorkerOptions.workerSrc ||\n pdfjs.GlobalWorkerOptions.workerSrc === 'pdf.worker.mjs'\n) {\n pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n}\n\nexport interface PdfViewProps {\n name: string\n /** Blob object URL of the PDF bytes. */\n src: string\n onDownload?: () => void\n}\n\nexport default function PdfView({ name, src, onDownload }: PdfViewProps) {\n const [pageCount, setPageCount] = useState<number | null>(null)\n const [pageNumber, setPageNumber] = useState(1)\n const [loadError, setLoadError] = useState(false)\n\n // Fit-width: render the page at the scroll area's current width.\n const scrollRef = useRef<HTMLDivElement | null>(null)\n const [pageWidth, setPageWidth] = useState<number | null>(null)\n useEffect(() => {\n const element = scrollRef.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setPageWidth(Math.floor(entry.contentRect.width))\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [])\n\n if (loadError) {\n return (\n <div className=\"flex h-full w-full items-center justify-center p-4\">\n <span className=\"font-sans text-[13px] text-danger\">\n Could not display this PDF. Download it instead.\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"flex h-full w-full flex-col\">\n <div ref={scrollRef} className=\"min-h-0 flex-1 overflow-auto\">\n <Document\n file={src}\n onLoadSuccess={({ numPages }) => {\n setPageCount(numPages)\n setPageNumber(1)\n }}\n onLoadError={() => setLoadError(true)}\n loading={\n <Skeleton aria-label={`Loading ${name}`} className=\"h-full w-full rounded-none\" />\n }\n error={null}\n >\n {pageWidth ? (\n <Page\n pageNumber={pageNumber}\n width={pageWidth}\n renderTextLayer={false}\n renderAnnotationLayer={false}\n />\n ) : null}\n </Document>\n </div>\n <div className=\"flex shrink-0 items-center justify-center gap-1.5 border-t border-rule bg-surface px-2 py-1\">\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Previous page\"\n isDisabled={pageNumber <= 1}\n onPress={() => setPageNumber((page) => Math.max(1, page - 1))}\n >\n <ChevronLeft aria-hidden />\n </Button>\n <span className=\"min-w-14 text-center font-sans text-[12px] tabular-nums text-ink-3\">\n {pageCount ? `${pageNumber} / ${pageCount}` : '–'}\n </span>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Next page\"\n isDisabled={pageCount === null || pageNumber >= pageCount}\n onPress={() => setPageNumber((page) => Math.min(pageCount ?? page, page + 1))}\n >\n <ChevronRight aria-hidden />\n </Button>\n {onDownload ? (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Download ${name}`}\n onPress={onDownload}\n >\n <Download aria-hidden />\n </Button>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteos/ui",
3
- "version": "0.61.0",
3
+ "version": "0.76.0",
4
4
  "license": "FSL-1.1-Apache-2.0",
5
5
  "description": "Proteos React component library (React Aria Components + Tailwind v4).",
6
6
  "repository": {
@@ -56,7 +56,7 @@
56
56
  "tailwind-variants": "^3.2.2",
57
57
  "tailwindcss": "^4.2.4",
58
58
  "thinking-orbs": "^0.3.1",
59
- "@proteos/sdk": "0.61.0"
59
+ "@proteos/sdk": "0.76.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@biomejs/biome": "^2.4.13",
@@ -0,0 +1,78 @@
1
+ import { type CalendarDate, getLocalTimeZone, today } from '@internationalized/date'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { useState } from 'react'
4
+
5
+ import { AvailabilityCalendar } from './availability-calendar'
6
+
7
+ const meta: Meta<typeof AvailabilityCalendar> = {
8
+ title: 'Components/AvailabilityCalendar',
9
+ component: AvailabilityCalendar,
10
+ tags: ['autodocs'],
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="w-[480px] p-6 bg-surface border border-rule rounded-md">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ }
19
+
20
+ export default meta
21
+ type Story = StoryObj<typeof AvailabilityCalendar>
22
+
23
+ const now = today(getLocalTimeZone())
24
+
25
+ /** Weekdays from today on, for the next six weeks. */
26
+ function isWeekdayAhead(date: CalendarDate): boolean {
27
+ const offset = date.compare(now)
28
+ if (offset < 0 || offset > 42) return false
29
+ const weekday = date.toDate(getLocalTimeZone()).getDay()
30
+ return weekday !== 0 && weekday !== 6
31
+ }
32
+
33
+ export const WeekdaysAvailable: Story = {
34
+ render: () => {
35
+ const [value, setValue] = useState<CalendarDate | null>(null)
36
+ return (
37
+ <AvailabilityCalendar
38
+ aria-label="Pick a day"
39
+ value={value}
40
+ onChange={setValue}
41
+ minValue={now}
42
+ isDateUnavailable={(date) => !isWeekdayAhead(date)}
43
+ />
44
+ )
45
+ },
46
+ }
47
+
48
+ export const Selected: Story = {
49
+ render: () => {
50
+ const [value, setValue] = useState<CalendarDate | null>(now.add({ days: 3 }))
51
+ return (
52
+ <AvailabilityCalendar
53
+ aria-label="Pick a day"
54
+ value={value}
55
+ onChange={setValue}
56
+ minValue={now}
57
+ isDateUnavailable={(date) => !isWeekdayAhead(date)}
58
+ />
59
+ )
60
+ },
61
+ }
62
+
63
+ export const Loading: Story = {
64
+ render: () => (
65
+ <AvailabilityCalendar aria-label="Pick a day" value={null} onChange={() => {}} isLoading />
66
+ ),
67
+ }
68
+
69
+ export const NothingAvailable: Story = {
70
+ render: () => (
71
+ <AvailabilityCalendar
72
+ aria-label="Pick a day"
73
+ value={null}
74
+ onChange={() => {}}
75
+ isDateUnavailable={() => true}
76
+ />
77
+ ),
78
+ }
@@ -0,0 +1,89 @@
1
+ import { CalendarDate } from '@internationalized/date'
2
+ import { render, screen } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { useState } from 'react'
5
+ import { describe, expect, it, vi } from 'vitest'
6
+
7
+ import { AvailabilityCalendar } from './availability-calendar'
8
+
9
+ const OCTOBER_FIRST = new CalendarDate(2026, 10, 1)
10
+ const AVAILABLE = new Set(['2026-10-20', '2026-10-21', '2026-10-23'])
11
+
12
+ function Harness({
13
+ initial = null,
14
+ onChange,
15
+ isLoading = false,
16
+ }: {
17
+ initial?: CalendarDate | null
18
+ onChange?: (date: CalendarDate) => void
19
+ isLoading?: boolean
20
+ }) {
21
+ const [value, setValue] = useState<CalendarDate | null>(initial)
22
+ return (
23
+ <AvailabilityCalendar
24
+ aria-label="Pick a day"
25
+ value={value}
26
+ onChange={(next) => {
27
+ setValue(next)
28
+ onChange?.(next)
29
+ }}
30
+ defaultFocusedValue={OCTOBER_FIRST}
31
+ minValue={new CalendarDate(2026, 10, 15)}
32
+ isDateUnavailable={(date) => !AVAILABLE.has(date.toString())}
33
+ isLoading={isLoading}
34
+ />
35
+ )
36
+ }
37
+
38
+ describe('AvailabilityCalendar', () => {
39
+ it('renders a labelled grid for the focused month', () => {
40
+ render(<Harness />)
41
+ expect(screen.getByRole('grid', { name: /Pick a day/ })).toBeInTheDocument()
42
+ expect(screen.getByText('October')).toBeInTheDocument()
43
+ expect(screen.getByText('2026')).toBeInTheDocument()
44
+ })
45
+
46
+ it('marks days without availability as unavailable but keeps them in the grid', () => {
47
+ render(<Harness />)
48
+ const available = screen.getByRole('button', { name: /October 20/ })
49
+ const unavailable = screen.getByRole('button', { name: /October 22/ })
50
+ expect(available).not.toHaveAttribute('aria-disabled', 'true')
51
+ expect(unavailable).toHaveAttribute('aria-disabled', 'true')
52
+ })
53
+
54
+ it('selects an available day on click and reports it', async () => {
55
+ const user = userEvent.setup()
56
+ const onChange = vi.fn()
57
+ render(<Harness onChange={onChange} />)
58
+ await user.click(screen.getByRole('button', { name: /October 23/ }))
59
+ expect(onChange).toHaveBeenLastCalledWith(new CalendarDate(2026, 10, 23))
60
+ expect(screen.getByRole('gridcell', { selected: true })).toHaveTextContent('23')
61
+ })
62
+
63
+ it('ignores clicks on unavailable days', async () => {
64
+ const user = userEvent.setup()
65
+ const onChange = vi.fn()
66
+ render(<Harness onChange={onChange} />)
67
+ await user.click(screen.getByRole('button', { name: /October 22/ }))
68
+ expect(onChange).not.toHaveBeenCalled()
69
+ })
70
+
71
+ it('refuses selection while loading and flags the grid busy', async () => {
72
+ const user = userEvent.setup()
73
+ const onChange = vi.fn()
74
+ render(<Harness onChange={onChange} isLoading />)
75
+ expect(screen.getByRole('grid', { name: /Pick a day/ }).closest('[aria-busy]')).toHaveAttribute(
76
+ 'aria-busy',
77
+ 'true',
78
+ )
79
+ await user.click(screen.getByRole('button', { name: /October 20/ }))
80
+ expect(onChange).not.toHaveBeenCalled()
81
+ })
82
+
83
+ it('moves to the next month with the nav button', async () => {
84
+ const user = userEvent.setup()
85
+ render(<Harness />)
86
+ await user.click(screen.getByRole('button', { name: 'Next month' }))
87
+ expect(screen.getByText('November')).toBeInTheDocument()
88
+ })
89
+ })
@@ -0,0 +1,182 @@
1
+ import type { CalendarDate, DateValue } from '@internationalized/date'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { useContext } from 'react'
4
+ import {
5
+ Button as AriaButton,
6
+ Calendar as AriaCalendar,
7
+ type CalendarProps as AriaCalendarProps,
8
+ CalendarCell,
9
+ CalendarGrid,
10
+ CalendarGridBody,
11
+ CalendarGridHeader,
12
+ CalendarHeaderCell,
13
+ CalendarStateContext,
14
+ Heading,
15
+ } from 'react-aria-components'
16
+ import { tv, type VariantProps } from 'tailwind-variants'
17
+
18
+ import { cn } from '../../lib/cn'
19
+ import { Skeleton } from '../skeleton/skeleton'
20
+
21
+ /* =========================================================================
22
+ AvailabilityCalendar — a page-level month grid that marks which days have
23
+ something to offer (free times, open seats, stock). Distinct from the
24
+ popover calendar inside DatePicker: square cells, the available days
25
+ carry a tint, the selected day is the one ink fill, unavailable days are
26
+ quiet text, today is a dot. Wraps RAC Calendar so the grid is a real
27
+ `grid` with roving focus, arrow / PageUp / Home keys and announced cells.
28
+
29
+ "Available" is the consumer's call through `isDateUnavailable` — the
30
+ component only paints the answer. Loading paints every cell as a
31
+ skeleton and refuses selection until the consumer knows.
32
+ ========================================================================= */
33
+
34
+ export const availabilityCalendarStyles = tv({
35
+ slots: {
36
+ root: 'flex flex-col',
37
+ header: 'flex items-center justify-between mb-3.5',
38
+ heading: 'font-sans text-[15px] text-ink tracking-[-0.01em]',
39
+ headingYear: 'ml-1 text-ink-3',
40
+ nav: 'flex gap-1',
41
+ navButton: [
42
+ 'size-7 rounded-md inline-flex items-center justify-center cursor-pointer outline-none',
43
+ 'text-ink-3 hovered:text-ink hovered:bg-surface-3 pressed:bg-surface-4',
44
+ 'data-[focus-visible]:shadow-[var(--shadow-focus)]',
45
+ 'data-[disabled]:opacity-40 data-[disabled]:cursor-default data-[disabled]:hover:bg-transparent',
46
+ ],
47
+ grid: 'w-full border-separate border-spacing-1 table-fixed -m-1',
48
+ weekday:
49
+ 'font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-ink-3 pb-2 text-center',
50
+ cell: [
51
+ 'relative aspect-square flex items-center justify-center cursor-pointer outline-none',
52
+ 'font-sans text-[14px] font-medium rounded-sm border border-transparent',
53
+ 'transition-[background-color,color,border-color] duration-150',
54
+ // Available (the default when neither unavailable nor disabled).
55
+ 'bg-surface-2 text-ink',
56
+ 'data-[hovered]:bg-surface-3 data-[hovered]:border-rule-strong',
57
+ 'data-[focus-visible]:shadow-[var(--shadow-focus)]',
58
+ // Selected: the one ink fill on the page.
59
+ 'data-[selected]:bg-ink data-[selected]:text-surface data-[selected]:border-ink',
60
+ 'data-[selected]:data-[hovered]:bg-ink data-[selected]:data-[hovered]:border-ink',
61
+ // Unavailable / past: quiet text, no fill, no hover.
62
+ 'data-[unavailable]:bg-transparent data-[unavailable]:text-ink-4 data-[unavailable]:font-normal data-[unavailable]:cursor-default',
63
+ 'data-[unavailable]:data-[hovered]:bg-transparent data-[unavailable]:data-[hovered]:border-transparent',
64
+ 'data-[disabled]:bg-transparent data-[disabled]:text-ink-4 data-[disabled]:font-normal data-[disabled]:cursor-default',
65
+ 'data-[disabled]:data-[hovered]:bg-transparent data-[disabled]:data-[hovered]:border-transparent',
66
+ // Days of the neighbouring months are blank in a month-aligned grid.
67
+ 'data-[outside-month]:invisible',
68
+ ],
69
+ dot: 'absolute bottom-1.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-current',
70
+ cellSkeleton: 'absolute inset-0 rounded-sm',
71
+ },
72
+ variants: {
73
+ isLoading: {
74
+ true: { cell: 'bg-transparent cursor-default' },
75
+ false: {},
76
+ },
77
+ },
78
+ defaultVariants: { isLoading: false },
79
+ })
80
+
81
+ export type AvailabilityCalendarVariants = VariantProps<typeof availabilityCalendarStyles>
82
+
83
+ export type AvailabilityCalendarProps = Omit<
84
+ AriaCalendarProps<CalendarDate>,
85
+ 'children' | 'value' | 'onChange' | 'className' | 'style' | 'visibleDuration'
86
+ > & {
87
+ /** The selected day; `null` = none. */
88
+ value: CalendarDate | null
89
+ onChange: (date: CalendarDate) => void
90
+ /** Paint every cell as a skeleton and refuse selection. */
91
+ isLoading?: boolean
92
+ /** Accessible name of the grid, e.g. "Pick a day". */
93
+ 'aria-label': string
94
+ /** Weekday header style; `short` = "Sun". */
95
+ weekdayStyle?: 'narrow' | 'short' | 'long'
96
+ className?: string
97
+ }
98
+
99
+ export function AvailabilityCalendar({
100
+ value,
101
+ onChange,
102
+ isLoading = false,
103
+ className,
104
+ isDateUnavailable,
105
+ weekdayStyle = 'short',
106
+ ...props
107
+ }: AvailabilityCalendarProps) {
108
+ const styles = availabilityCalendarStyles({ isLoading })
109
+ const unavailable = isLoading ? () => true : isDateUnavailable
110
+ return (
111
+ <AriaCalendar
112
+ {...props}
113
+ value={value}
114
+ onChange={(next) => onChange(next)}
115
+ {...(unavailable ? { isDateUnavailable: unavailable } : {})}
116
+ visibleDuration={{ months: 1 }}
117
+ className={cn(styles.root(), className)}
118
+ >
119
+ <header className={styles.header()}>
120
+ <Heading className={styles.heading()}>
121
+ <MonthHeading yearClassName={styles.headingYear()} />
122
+ </Heading>
123
+ <div className={styles.nav()}>
124
+ <AriaButton slot="previous" aria-label="Previous month" className={styles.navButton()}>
125
+ <ChevronLeft className="size-4" aria-hidden="true" />
126
+ </AriaButton>
127
+ <AriaButton slot="next" aria-label="Next month" className={styles.navButton()}>
128
+ <ChevronRight className="size-4" aria-hidden="true" />
129
+ </AriaButton>
130
+ </div>
131
+ </header>
132
+ {/* RAC filters aria-busy off the grid; the wrapper carries it. */}
133
+ <div aria-busy={isLoading}>
134
+ <CalendarGrid weekdayStyle={weekdayStyle} className={styles.grid()}>
135
+ <CalendarGridHeader>
136
+ {(day) => <CalendarHeaderCell className={styles.weekday()}>{day}</CalendarHeaderCell>}
137
+ </CalendarGridHeader>
138
+ <CalendarGridBody>
139
+ {(date) => (
140
+ <CalendarCell date={date} className={styles.cell()}>
141
+ {({ formattedDate, isToday, isOutsideMonth }) =>
142
+ isLoading && !isOutsideMonth ? (
143
+ <Skeleton className={styles.cellSkeleton()} />
144
+ ) : (
145
+ <>
146
+ {formattedDate}
147
+ {isToday && <span className={styles.dot()} aria-hidden="true" />}
148
+ </>
149
+ )
150
+ }
151
+ </CalendarCell>
152
+ )}
153
+ </CalendarGridBody>
154
+ </CalendarGrid>
155
+ </div>
156
+ </AriaCalendar>
157
+ )
158
+ }
159
+
160
+ /** "October 2026" as "October" + a muted year, read off the calendar state so
161
+ * it follows the visible month. RAC's Heading context supplies the plain
162
+ * title; our children override it, the accessible name stays on the grid. */
163
+ function MonthHeading({ yearClassName }: { yearClassName: string }) {
164
+ const state = useContext(CalendarStateContext)
165
+ if (!state) return null
166
+ const start: DateValue = state.visibleRange.start
167
+ const date = start.toDate(state.timeZone)
168
+ const month = new Intl.DateTimeFormat(undefined, {
169
+ month: 'long',
170
+ timeZone: state.timeZone,
171
+ }).format(date)
172
+ const year = new Intl.DateTimeFormat(undefined, {
173
+ year: 'numeric',
174
+ timeZone: state.timeZone,
175
+ }).format(date)
176
+ return (
177
+ <>
178
+ <span className="font-medium">{month}</span>
179
+ <span className={yearClassName}>{year}</span>
180
+ </>
181
+ )
182
+ }
@@ -0,0 +1,5 @@
1
+ export type {
2
+ AvailabilityCalendarProps,
3
+ AvailabilityCalendarVariants,
4
+ } from './availability-calendar'
5
+ export { AvailabilityCalendar, availabilityCalendarStyles } from './availability-calendar'
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
 
3
- import { Button, buttonStyles, type ButtonVariants } from './button'
3
+ import { Button, type ButtonVariants, buttonStyles } from './button'
4
4
 
5
5
  const meta: Meta<typeof Button> = {
6
6
  title: 'Components/Button',
@@ -151,6 +151,27 @@ export const Danger: Story = {
151
151
  ),
152
152
  }
153
153
 
154
+ /** A save in flight: a spinner before the label (in place of the icon on
155
+ * the icon sizes), presses ignored, still focusable and announced busy. */
156
+ export const Pending: Story = {
157
+ render: () => (
158
+ <div className="flex flex-wrap items-center gap-3">
159
+ <Button variant="cta" size="sm" isPending>
160
+ Create Deal
161
+ </Button>
162
+ <Button size="sm" isPending>
163
+ Apply 3 changes
164
+ </Button>
165
+ <Button variant="ghost" size="sm" isPending>
166
+ Send Ava now
167
+ </Button>
168
+ <Button size="icon-md" aria-label="Add" isPending>
169
+ <Plus />
170
+ </Button>
171
+ </div>
172
+ ),
173
+ }
174
+
154
175
  const columns = 'grid grid-cols-[160px_1fr] items-center gap-y-5 gap-x-6'
155
176
  const label = 'font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4'
156
177
 
@@ -47,7 +47,16 @@ describe('Button', () => {
47
47
  })
48
48
 
49
49
  it('renders every variant as a button', () => {
50
- const variants = ['cta', 'accent', 'primary', 'secondary', 'ghost', 'subtle', 'link', 'danger'] as const
50
+ const variants = [
51
+ 'cta',
52
+ 'accent',
53
+ 'primary',
54
+ 'secondary',
55
+ 'ghost',
56
+ 'subtle',
57
+ 'link',
58
+ 'danger',
59
+ ] as const
51
60
  render(
52
61
  <>
53
62
  {variants.map((variant) => (
@@ -89,4 +98,39 @@ describe('Button', () => {
89
98
  it('defaults to the primary variant', () => {
90
99
  expect(buttonStyles({})).toBe(buttonStyles({ variant: 'primary' }))
91
100
  })
101
+
102
+ describe('pending', () => {
103
+ it('shows a spinner before the label and ignores presses', async () => {
104
+ const user = userEvent.setup()
105
+ const onPress = vi.fn()
106
+ render(
107
+ <Button isPending onPress={onPress}>
108
+ Create Deal
109
+ </Button>,
110
+ )
111
+ const button = screen.getByRole('button', { name: 'Create Deal' })
112
+ expect(button).toHaveAttribute('data-pending', 'true')
113
+ expect(button.querySelector('[data-slot="pending-spinner"]')).not.toBeNull()
114
+ await user.click(button)
115
+ expect(onPress).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it('replaces the icon on an icon-only button', () => {
119
+ render(
120
+ <Button size="icon-md" aria-label="Add" isPending>
121
+ <span data-testid="icon" />
122
+ </Button>,
123
+ )
124
+ const button = screen.getByRole('button', { name: 'Add' })
125
+ expect(button.querySelector('[data-slot="pending-spinner"]')).not.toBeNull()
126
+ expect(screen.queryByTestId('icon')).not.toBeInTheDocument()
127
+ })
128
+
129
+ it('renders no spinner when idle', () => {
130
+ render(<Button>Save</Button>)
131
+ expect(
132
+ screen.getByRole('button', { name: 'Save' }).querySelector('[data-slot="pending-spinner"]'),
133
+ ).toBeNull()
134
+ })
135
+ })
92
136
  })