From f135b4b01181f289325b54f5eaf0758853311abc Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 13 Oct 2025 10:03:11 -0700 Subject: [PATCH 1/2] feat: add presence runtime validator functions --- canvas/package-lock.json | 26 +- canvas/package.json | 3 +- canvas/src/output.css | 2230 +++++++++-------- .../src/presence/Interfaces/CursorManager.ts | 16 +- canvas/src/presence/Interfaces/DragManager.ts | 19 +- canvas/src/presence/Interfaces/InkManager.ts | 27 +- .../presence/Interfaces/PresenceManager.ts | 20 +- .../src/presence/Interfaces/ResizeManager.ts | 19 +- .../presence/Interfaces/SelectionManager.ts | 20 +- .../src/presence/Interfaces/UsersManager.ts | 17 +- canvas/src/presence/cursor.ts | 27 +- canvas/src/presence/drag.ts | 39 +- canvas/src/presence/ink.ts | 36 +- canvas/src/presence/resize.ts | 25 +- canvas/src/presence/selection.ts | 94 +- canvas/src/presence/users.ts | 26 +- canvas/src/presence/validators.ts | 200 ++ 17 files changed, 1545 insertions(+), 1299 deletions(-) create mode 100644 canvas/src/presence/validators.ts diff --git a/canvas/package-lock.json b/canvas/package-lock.json index abcb2171..90b44122 100644 --- a/canvas/package-lock.json +++ b/canvas/package-lock.json @@ -44,7 +44,8 @@ "react-dom": "^18.3.1", "unique-names-generator": "^4.7.1", "uuid": "^11.0.5", - "vite": "^7.0.1" + "vite": "^7.0.1", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -4073,29 +4074,6 @@ } } }, - "node_modules/@langchain/openai/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@langchain/textsplitters": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", diff --git a/canvas/package.json b/canvas/package.json index 4ef60cca..02831f30 100644 --- a/canvas/package.json +++ b/canvas/package.json @@ -63,7 +63,8 @@ "react-dom": "^18.3.1", "unique-names-generator": "^4.7.1", "uuid": "^11.0.5", - "vite": "^7.0.1" + "vite": "^7.0.1", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/canvas/src/output.css b/canvas/src/output.css index cec92dc3..867e3628 100644 --- a/canvas/src/output.css +++ b/canvas/src/output.css @@ -2,1163 +2,1283 @@ @layer properties; @layer theme, base, components, utilities; @layer theme { - :root, :host { - --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; - --color-red-100: oklch(93.6% 0.032 17.717); - --color-red-500: oklch(63.7% 0.237 25.331); - --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-800: oklch(44.4% 0.177 26.899); - --color-green-100: oklch(96.2% 0.044 156.743); - --color-green-500: oklch(72.3% 0.219 149.579); - --color-green-700: oklch(52.7% 0.154 150.069); - --color-blue-100: oklch(93.2% 0.032 255.585); - --color-blue-500: oklch(62.3% 0.214 259.815); - --color-blue-600: oklch(54.6% 0.245 262.881); - --color-blue-700: oklch(48.8% 0.243 264.376); - --color-indigo-100: oklch(93% 0.034 272.788); - --color-gray-100: oklch(96.7% 0.003 264.542); - --color-gray-200: oklch(92.8% 0.006 264.531); - --color-gray-300: oklch(87.2% 0.01 258.338); - --color-gray-400: oklch(70.7% 0.022 261.325); - --color-gray-500: oklch(55.1% 0.027 264.364); - --color-gray-600: oklch(44.6% 0.03 256.802); - --color-gray-700: oklch(37.3% 0.034 259.733); - --color-gray-800: oklch(27.8% 0.033 256.848); - --color-black: #000; - --color-white: #fff; - --spacing: 0.25rem; - --text-xs: 0.75rem; - --text-xs--line-height: calc(1 / 0.75); - --text-sm: 0.875rem; - --text-sm--line-height: calc(1.25 / 0.875); - --text-base: 1rem; - --text-base--line-height: calc(1.5 / 1); - --text-lg: 1.125rem; - --text-lg--line-height: calc(1.75 / 1.125); - --font-weight-medium: 500; - --font-weight-semibold: 600; - --radius-sm: 0.25rem; - --radius-md: 0.375rem; - --radius-xl: 0.75rem; - --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); - --ease-out: cubic-bezier(0, 0, 0.2, 1); - --default-transition-duration: 150ms; - --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - --default-font-family: var(--font-sans); - --default-mono-font-family: var(--font-mono); - } + :root, + :host { + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-indigo-100: oklch(93% 0.034 272.788); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-xl: 0.75rem; + --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } } @layer base { - *, ::after, ::before, ::backdrop, ::file-selector-button { - box-sizing: border-box; - margin: 0; - padding: 0; - border: 0 solid; - } - html, :host { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - tab-size: 4; - font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); - font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var(--default-font-variation-settings, normal); - -webkit-tap-highlight-color: transparent; - } - hr { - height: 0; - color: inherit; - border-top-width: 1px; - } - abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - } - h1, h2, h3, h4, h5, h6 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; - } - b, strong { - font-weight: bolder; - } - code, kbd, samp, pre { - font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); - font-feature-settings: var(--default-mono-font-feature-settings, normal); - font-variation-settings: var(--default-mono-font-variation-settings, normal); - font-size: 1em; - } - small { - font-size: 80%; - } - sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - sub { - bottom: -0.25em; - } - sup { - top: -0.5em; - } - table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; - } - :-moz-focusring { - outline: auto; - } - progress { - vertical-align: baseline; - } - summary { - display: list-item; - } - ol, ul, menu { - list-style: none; - } - img, svg, video, canvas, audio, iframe, embed, object { - display: block; - vertical-align: middle; - } - img, video { - max-width: 100%; - height: auto; - } - button, input, select, optgroup, textarea, ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - border-radius: 0; - background-color: transparent; - opacity: 1; - } - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - ::file-selector-button { - margin-inline-end: 4px; - } - ::placeholder { - opacity: 1; - } - @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentcolor; - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - textarea { - resize: vertical; - } - ::-webkit-search-decoration { - -webkit-appearance: none; - } - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - ::-webkit-datetime-edit { - display: inline-flex; - } - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - :-moz-ui-invalid { - box-shadow: none; - } - button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { - appearance: button; - } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { - height: auto; - } - [hidden]:where(:not([hidden="until-found"])) { - display: none !important; - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, + :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji" + ); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, + strong { + font-weight: bolder; + } + code, + kbd, + samp, + pre { + font-family: var( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + "Liberation Mono", + "Courier New", + monospace + ); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, + ul, + menu { + list-style: none; + } + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + vertical-align: middle; + } + img, + video { + max-width: 100%; + height: auto; + } + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, + input:where([type="button"], [type="reset"], [type="submit"]), + ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } } @layer utilities { - .pointer-events-auto { - pointer-events: auto; - } - .pointer-events-none { - pointer-events: none; - } - .invisible { - visibility: hidden; - } - .visible { - visibility: visible; - } - .absolute { - position: absolute; - } - .fixed { - position: fixed; - } - .relative { - position: relative; - } - .static { - position: static; - } - .sticky { - position: sticky; - } - .inset-0 { - inset: calc(var(--spacing) * 0); - } - .top-0 { - top: calc(var(--spacing) * 0); - } - .top-4 { - top: calc(var(--spacing) * 4); - } - .left-0 { - left: calc(var(--spacing) * 0); - } - .left-2 { - left: calc(var(--spacing) * 2); - } - .left-6 { - left: calc(var(--spacing) * 6); - } - .isolate { - isolation: isolate; - } - .z-0 { - z-index: 0; - } - .z-1 { - z-index: 1; - } - .z-2 { - z-index: 2; - } - .z-10 { - z-index: 10; - } - .z-20 { - z-index: 20; - } - .z-100 { - z-index: 100; - } - .z-\[1001\] { - z-index: 1001; - } - .z-\[9998\] { - z-index: 9998; - } - .z-\[9999\] { - z-index: 9999; - } - .container { - width: 100%; - @media (width >= 40rem) { - max-width: 40rem; - } - @media (width >= 48rem) { - max-width: 48rem; - } - @media (width >= 64rem) { - max-width: 64rem; - } - @media (width >= 80rem) { - max-width: 80rem; - } - @media (width >= 96rem) { - max-width: 96rem; - } - } - .m-2 { - margin: calc(var(--spacing) * 2); - } - .m-4 { - margin: calc(var(--spacing) * 4); - } - .mx-auto { - margin-inline: auto; - } - .mt-2 { - margin-top: calc(var(--spacing) * 2); - } - .mt-4 { - margin-top: calc(var(--spacing) * 4); - } - .mr-1 { - margin-right: calc(var(--spacing) * 1); - } - .mr-2 { - margin-right: calc(var(--spacing) * 2); - } - .mr-4 { - margin-right: calc(var(--spacing) * 4); - } - .mr-6 { - margin-right: calc(var(--spacing) * 6); - } - .mr-8 { - margin-right: calc(var(--spacing) * 8); - } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); - } - .ml-1 { - margin-left: calc(var(--spacing) * 1); - } - .ml-2 { - margin-left: calc(var(--spacing) * 2); - } - .ml-4 { - margin-left: calc(var(--spacing) * 4); - } - .ml-6 { - margin-left: calc(var(--spacing) * 6); - } - .block { - display: block; - } - .flex { - display: flex; - } - .grid { - display: grid; - } - .hidden { - display: none; - } - .inline { - display: inline; - } - .inline-flex { - display: inline-flex; - } - .table { - display: table; - } - .table-cell { - display: table-cell; - } - .table-row { - display: table-row; - } - .h-4 { - height: calc(var(--spacing) * 4); - } - .h-6 { - height: calc(var(--spacing) * 6); - } - .h-12 { - height: calc(var(--spacing) * 12); - } - .h-\[48px\] { - height: 48px; - } - .h-\[calc\(100vh-96px\)\] { - height: calc(100vh - 96px); - } - .h-auto { - height: auto; - } - .h-full { - height: 100%; - } - .h-screen { - height: 100vh; - } - .max-h-\[48px\] { - max-height: 48px; - } - .min-h-\[36px\] { - min-height: 36px; - } - .min-h-\[48px\] { - min-height: 48px; - } - .w-4 { - width: calc(var(--spacing) * 4); - } - .w-6 { - width: calc(var(--spacing) * 6); - } - .w-12 { - width: calc(var(--spacing) * 12); - } - .w-24 { - width: calc(var(--spacing) * 24); - } - .w-56 { - width: calc(var(--spacing) * 56); - } - .w-full { - width: 100%; - } - .max-w-80 { - max-width: calc(var(--spacing) * 80); - } - .min-w-80 { - min-width: calc(var(--spacing) * 80); - } - .flex-shrink { - flex-shrink: 1; - } - .shrink-0 { - flex-shrink: 0; - } - .flex-grow { - flex-grow: 1; - } - .grow { - flex-grow: 1; - } - .table-auto { - table-layout: auto; - } - .border-collapse { - border-collapse: collapse; - } - .transform { - transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); - } - .cursor-grab { - cursor: grab; - } - .cursor-nw-resize { - cursor: nw-resize; - } - .cursor-pointer { - cursor: pointer; - } - .resize { - resize: both; - } - .flex-col { - flex-direction: column; - } - .flex-row { - flex-direction: row; - } - .flex-nowrap { - flex-wrap: nowrap; - } - .flex-wrap { - flex-wrap: wrap; - } - .items-center { - align-items: center; - } - .justify-between { - justify-content: space-between; - } - .justify-center { - justify-content: center; - } - .justify-start { - justify-content: flex-start; - } - .gap-3 { - gap: calc(var(--spacing) * 3); - } - .gap-4 { - gap: calc(var(--spacing) * 4); - } - .space-y-2 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); - } - } - .gap-x-1 { - column-gap: calc(var(--spacing) * 1); - } - .gap-y-2 { - row-gap: calc(var(--spacing) * 2); - } - .justify-self-end { - justify-self: flex-end; - } - .truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .overflow-auto { - overflow: auto; - } - .overflow-hidden { - overflow: hidden; - } - .overflow-x-auto { - overflow-x: auto; - } - .overflow-y-auto { - overflow-y: auto; - } - .overflow-y-hidden { - overflow-y: hidden; - } - .overscroll-none { - overscroll-behavior: none; - } - .rounded { - border-radius: 0.25rem; - } - .rounded-full { - border-radius: calc(infinity * 1px); - } - .rounded-md { - border-radius: var(--radius-md); - } - .rounded-sm { - border-radius: var(--radius-sm); - } - .rounded-xl { - border-radius: var(--radius-xl); - } - .rounded-br-none { - border-bottom-right-radius: 0; - } - .rounded-bl-none { - border-bottom-left-radius: 0; - } - .border { - border-style: var(--tw-border-style); - border-width: 1px; - } - .border-2 { - border-style: var(--tw-border-style); - border-width: 2px; - } - .border-3 { - border-style: var(--tw-border-style); - border-width: 3px; - } - .border-r-1 { - border-right-style: var(--tw-border-style); - border-right-width: 1px; - } - .border-r-2 { - border-right-style: var(--tw-border-style); - border-right-width: 2px; - } - .border-b-2 { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 2px; - } - .border-l { - border-left-style: var(--tw-border-style); - border-left-width: 1px; - } - .border-l-2 { - border-left-style: var(--tw-border-style); - border-left-width: 2px; - } - .border-dashed { - --tw-border-style: dashed; - border-style: dashed; - } - .border-black { - border-color: var(--color-black); - } - .border-blue-500 { - border-color: var(--color-blue-500); - } - .border-gray-100 { - border-color: var(--color-gray-100); - } - .border-gray-200 { - border-color: var(--color-gray-200); - } - .border-gray-300 { - border-color: var(--color-gray-300); - } - .border-green-500 { - border-color: var(--color-green-500); - } - .border-white { - border-color: var(--color-white); - } - .border-white\/20 { - border-color: color-mix(in srgb, #fff 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 20%, transparent); - } - } - .bg-black { - background-color: var(--color-black); - } - .bg-black\/70 { - background-color: color-mix(in srgb, #000 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 70%, transparent); - } - } - .bg-blue-100 { - background-color: var(--color-blue-100); - } - .bg-blue-600 { - background-color: var(--color-blue-600); - } - .bg-gray-100 { - background-color: var(--color-gray-100); - } - .bg-gray-200 { - background-color: var(--color-gray-200); - } - .bg-gray-600 { - background-color: var(--color-gray-600); - } - .bg-green-100 { - background-color: var(--color-green-100); - } - .bg-indigo-100 { - background-color: var(--color-indigo-100); - } - .bg-red-100 { - background-color: var(--color-red-100); - } - .bg-transparent { - background-color: transparent; - } - .bg-white { - background-color: var(--color-white); - } - .p-1 { - padding: calc(var(--spacing) * 1); - } - .p-2 { - padding: calc(var(--spacing) * 2); - } - .p-4 { - padding: calc(var(--spacing) * 4); - } - .px-1 { - padding-inline: calc(var(--spacing) * 1); - } - .px-2 { - padding-inline: calc(var(--spacing) * 2); - } - .px-3 { - padding-inline: calc(var(--spacing) * 3); - } - .px-4 { - padding-inline: calc(var(--spacing) * 4); - } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } - .py-2 { - padding-block: calc(var(--spacing) * 2); - } - .pr-4 { - padding-right: calc(var(--spacing) * 4); - } - .pl-4 { - padding-left: calc(var(--spacing) * 4); - } - .text-base { - font-size: var(--text-base); - line-height: var(--tw-leading, var(--text-base--line-height)); - } - .text-lg { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - } - .text-sm { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - .text-xs { - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - } - .text-\[10px\] { - font-size: 10px; - } - .font-medium { - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - } - .font-semibold { - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - } - .text-nowrap { - text-wrap: nowrap; - } - .text-wrap { - text-wrap: wrap; - } - .whitespace-nowrap { - white-space: nowrap; - } - .text-black { - color: var(--color-black); - } - .text-blue-500 { - color: var(--color-blue-500); - } - .text-blue-700 { - color: var(--color-blue-700); - } - .text-gray-400 { - color: var(--color-gray-400); - } - .text-gray-500 { - color: var(--color-gray-500); - } - .text-gray-600 { - color: var(--color-gray-600); - } - .text-green-700 { - color: var(--color-green-700); - } - .text-red-500 { - color: var(--color-red-500); - } - .text-red-600 { - color: var(--color-red-600); - } - .text-white { - color: var(--color-white); - } - .capitalize { - text-transform: capitalize; - } - .tabular-nums { - --tw-numeric-spacing: tabular-nums; - font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); - } - .underline { - text-decoration-line: underline; - } - .opacity-10 { - opacity: 10%; - } - .opacity-50 { - opacity: 50%; - } - .shadow-lg { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-md { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } - .outline-2 { - outline-style: var(--tw-outline-style); - outline-width: 2px; - } - .-outline-offset-2 { - outline-offset: calc(2px * -1); - } - .outline-blue-600 { - outline-color: var(--color-blue-600); - } - .outline-red-800 { - outline-color: var(--color-red-800); - } - .drop-shadow-md { - --tw-drop-shadow-size: drop-shadow(0 3px 3px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.12))); - --tw-drop-shadow: drop-shadow(var(--drop-shadow-md)); - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-all { - transition-property: all; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-colors { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .duration-200 { - --tw-duration: 200ms; - transition-duration: 200ms; - } - .duration-300 { - --tw-duration: 300ms; - transition-duration: 300ms; - } - .ease-out { - --tw-ease: var(--ease-out); - transition-timing-function: var(--ease-out); - } - .outline-none { - --tw-outline-style: none; - outline-style: none; - } - .select-none { - -webkit-user-select: none; - user-select: none; - } - .selection\:table { - & *::selection { - display: table; - } - &::selection { - display: table; - } - } - .odd\:bg-gray-100 { - &:nth-child(odd) { - background-color: var(--color-gray-100); - } - } - .even\:bg-white { - &:nth-child(even) { - background-color: var(--color-white); - } - } - .hover\:bg-black { - &:hover { - @media (hover: hover) { - background-color: var(--color-black); - } - } - } - .hover\:bg-gray-400 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-400); - } - } - } - .hover\:bg-gray-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-700); - } - } - } - .hover\:bg-gray-800 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-800); - } - } - } - .hover\:text-gray-800 { - &:hover { - @media (hover: hover) { - color: var(--color-gray-800); - } - } - } - .hover\:underline { - &:hover { - @media (hover: hover) { - text-decoration-line: underline; - } - } - } - .hover\:shadow-xl { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } + .pointer-events-auto { + pointer-events: auto; + } + .pointer-events-none { + pointer-events: none; + } + .invisible { + visibility: hidden; + } + .visible { + visibility: visible; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .sticky { + position: sticky; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-4 { + top: calc(var(--spacing) * 4); + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .left-2 { + left: calc(var(--spacing) * 2); + } + .left-6 { + left: calc(var(--spacing) * 6); + } + .isolate { + isolation: isolate; + } + .z-0 { + z-index: 0; + } + .z-1 { + z-index: 1; + } + .z-2 { + z-index: 2; + } + .z-10 { + z-index: 10; + } + .z-20 { + z-index: 20; + } + .z-100 { + z-index: 100; + } + .z-\[1001\] { + z-index: 1001; + } + .z-\[9998\] { + z-index: 9998; + } + .z-\[9999\] { + z-index: 9999; + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .m-2 { + margin: calc(var(--spacing) * 2); + } + .m-4 { + margin: calc(var(--spacing) * 4); + } + .mx-auto { + margin-inline: auto; + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } + .mr-4 { + margin-right: calc(var(--spacing) * 4); + } + .mr-6 { + margin-right: calc(var(--spacing) * 6); + } + .mr-8 { + margin-right: calc(var(--spacing) * 8); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-6 { + margin-left: calc(var(--spacing) * 6); + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .inline-flex { + display: inline-flex; + } + .table { + display: table; + } + .table-cell { + display: table-cell; + } + .table-row { + display: table-row; + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-\[48px\] { + height: 48px; + } + .h-\[calc\(100vh-96px\)\] { + height: calc(100vh - 96px); + } + .h-auto { + height: auto; + } + .h-full { + height: 100%; + } + .h-screen { + height: 100vh; + } + .max-h-\[48px\] { + max-height: 48px; + } + .min-h-\[36px\] { + min-height: 36px; + } + .min-h-\[48px\] { + min-height: 48px; + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-12 { + width: calc(var(--spacing) * 12); + } + .w-24 { + width: calc(var(--spacing) * 24); + } + .w-56 { + width: calc(var(--spacing) * 56); + } + .w-full { + width: 100%; + } + .max-w-80 { + max-width: calc(var(--spacing) * 80); + } + .min-w-80 { + min-width: calc(var(--spacing) * 80); + } + .flex-shrink { + flex-shrink: 1; + } + .shrink-0 { + flex-shrink: 0; + } + .flex-grow { + flex-grow: 1; + } + .grow { + flex-grow: 1; + } + .table-auto { + table-layout: auto; + } + .border-collapse { + border-collapse: collapse; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) + var(--tw-skew-y,); + } + .cursor-grab { + cursor: grab; + } + .cursor-nw-resize { + cursor: nw-resize; + } + .cursor-pointer { + cursor: pointer; + } + .resize { + resize: both; + } + .flex-col { + flex-direction: column; + } + .flex-row { + flex-direction: row; + } + .flex-nowrap { + flex-wrap: nowrap; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-start { + justify-content: flex-start; + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } + .gap-x-1 { + column-gap: calc(var(--spacing) * 1); + } + .gap-y-2 { + row-gap: calc(var(--spacing) * 2); + } + .justify-self-end { + justify-self: flex-end; + } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .overflow-auto { + overflow: auto; + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-auto { + overflow-x: auto; + } + .overflow-y-auto { + overflow-y: auto; + } + .overflow-y-hidden { + overflow-y: hidden; + } + .overscroll-none { + overscroll-behavior: none; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-sm { + border-radius: var(--radius-sm); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .rounded-br-none { + border-bottom-right-radius: 0; + } + .rounded-bl-none { + border-bottom-left-radius: 0; + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-2 { + border-style: var(--tw-border-style); + border-width: 2px; + } + .border-3 { + border-style: var(--tw-border-style); + border-width: 3px; + } + .border-r-1 { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-r-2 { + border-right-style: var(--tw-border-style); + border-right-width: 2px; + } + .border-b-2 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 2px; + } + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + .border-l-2 { + border-left-style: var(--tw-border-style); + border-left-width: 2px; + } + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } + .border-black { + border-color: var(--color-black); + } + .border-blue-500 { + border-color: var(--color-blue-500); + } + .border-gray-100 { + border-color: var(--color-gray-100); + } + .border-gray-200 { + border-color: var(--color-gray-200); + } + .border-gray-300 { + border-color: var(--color-gray-300); + } + .border-green-500 { + border-color: var(--color-green-500); + } + .border-white { + border-color: var(--color-white); + } + .border-white\/20 { + border-color: color-mix(in srgb, #fff 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 20%, transparent); + } + } + .bg-black { + background-color: var(--color-black); + } + .bg-black\/70 { + background-color: color-mix(in srgb, #000 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 70%, transparent); + } + } + .bg-blue-100 { + background-color: var(--color-blue-100); + } + .bg-blue-600 { + background-color: var(--color-blue-600); + } + .bg-gray-100 { + background-color: var(--color-gray-100); + } + .bg-gray-200 { + background-color: var(--color-gray-200); + } + .bg-gray-600 { + background-color: var(--color-gray-600); + } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-indigo-100 { + background-color: var(--color-indigo-100); + } + .bg-red-100 { + background-color: var(--color-red-100); + } + .bg-transparent { + background-color: transparent; + } + .bg-white { + background-color: var(--color-white); + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .px-1 { + padding-inline: calc(var(--spacing) * 1); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .pr-4 { + padding-right: calc(var(--spacing) * 4); + } + .pl-4 { + padding-left: calc(var(--spacing) * 4); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .text-\[10px\] { + font-size: 10px; + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .text-nowrap { + text-wrap: nowrap; + } + .text-wrap { + text-wrap: wrap; + } + .whitespace-nowrap { + white-space: nowrap; + } + .text-black { + color: var(--color-black); + } + .text-blue-500 { + color: var(--color-blue-500); + } + .text-blue-700 { + color: var(--color-blue-700); + } + .text-gray-400 { + color: var(--color-gray-400); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-600 { + color: var(--color-gray-600); + } + .text-green-700 { + color: var(--color-green-700); + } + .text-red-500 { + color: var(--color-red-500); + } + .text-red-600 { + color: var(--color-red-600); + } + .text-white { + color: var(--color-white); + } + .capitalize { + text-transform: capitalize; + } + .tabular-nums { + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) + var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); + } + .underline { + text-decoration-line: underline; + } + .opacity-10 { + opacity: 10%; + } + .opacity-50 { + opacity: 50%; + } + .shadow-lg { + --tw-shadow: + 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), + 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), + var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-md { + --tw-shadow: + 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), + 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), + var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: + 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), + 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), + var(--tw-ring-shadow), var(--tw-shadow); + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .outline-2 { + outline-style: var(--tw-outline-style); + outline-width: 2px; + } + .-outline-offset-2 { + outline-offset: calc(2px * -1); + } + .outline-blue-600 { + outline-color: var(--color-blue-600); + } + .outline-red-800 { + outline-color: var(--color-red-800); + } + .drop-shadow-md { + --tw-drop-shadow-size: drop-shadow( + 0 3px 3px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.12)) + ); + --tw-drop-shadow: drop-shadow(var(--drop-shadow-md)); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) + var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) + var(--tw-drop-shadow,); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) + var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) + var(--tw-drop-shadow,); + } + .transition { + transition-property: + color, + background-color, + border-color, + outline-color, + text-decoration-color, + fill, + stroke, + --tw-gradient-from, + --tw-gradient-via, + --tw-gradient-to, + opacity, + box-shadow, + transform, + translate, + scale, + rotate, + filter, + -webkit-backdrop-filter, + backdrop-filter, + display, + content-visibility, + overlay, + pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-colors { + transition-property: + color, background-color, border-color, outline-color, text-decoration-color, fill, + stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } + .outline-none { + --tw-outline-style: none; + outline-style: none; + } + .select-none { + -webkit-user-select: none; + user-select: none; + } + .selection\:table { + & *::selection { + display: table; + } + &::selection { + display: table; + } + } + .odd\:bg-gray-100 { + &:nth-child(odd) { + background-color: var(--color-gray-100); + } + } + .even\:bg-white { + &:nth-child(even) { + background-color: var(--color-white); + } + } + .hover\:bg-black { + &:hover { + @media (hover: hover) { + background-color: var(--color-black); + } + } + } + .hover\:bg-gray-400 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-400); + } + } + } + .hover\:bg-gray-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-700); + } + } + } + .hover\:bg-gray-800 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-800); + } + } + } + .hover\:text-gray-800 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-800); + } + } + } + .hover\:underline { + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } + } + .hover\:shadow-xl { + &:hover { + @media (hover: hover) { + --tw-shadow: + 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), + 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), + var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } } @layer base { - *, ::after, ::before, ::backdrop, ::file-selector-button { - border-color: var(--color-gray-200, currentColor); - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } } @keyframes slideInFromLeft { - from { - opacity: 0; - transform: translateX(-12px); - } - to { - opacity: 1; - transform: translateX(0); - } + from { + opacity: 0; + transform: translateX(-12px); + } + to { + opacity: 1; + transform: translateX(0); + } } .toolbar-slide-in { - animation: slideInFromLeft 0.25s ease-out forwards; + animation: slideInFromLeft 0.25s ease-out forwards; } .toolbar-slide-in-delayed { - animation: slideInFromLeft 0.25s ease-out 0.1s forwards; - opacity: 0; + animation: slideInFromLeft 0.25s ease-out 0.1s forwards; + opacity: 0; } .fui-Toolbar { - overflow-y: hidden !important; + overflow-y: hidden !important; } .fui-SwatchPicker { - overflow-y: hidden !important; + overflow-y: hidden !important; } @property --tw-rotate-x { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-rotate-y { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-rotate-z { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-skew-x { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-skew-y { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-space-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; + syntax: "*"; + inherits: false; + initial-value: 0; } @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; + syntax: "*"; + inherits: false; + initial-value: solid; } @property --tw-font-weight { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-ordinal { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-slashed-zero { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-numeric-figure { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-numeric-spacing { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-numeric-fraction { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } @property --tw-shadow-color { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; + syntax: ""; + inherits: false; + initial-value: 100%; } @property --tw-inset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } @property --tw-inset-shadow-color { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; + syntax: ""; + inherits: false; + initial-value: 100%; } @property --tw-ring-color { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } @property --tw-inset-ring-color { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-inset-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } @property --tw-ring-inset { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-ring-offset-width { - syntax: ""; - inherits: false; - initial-value: 0px; + syntax: ""; + inherits: false; + initial-value: 0px; } @property --tw-ring-offset-color { - syntax: "*"; - inherits: false; - initial-value: #fff; + syntax: "*"; + inherits: false; + initial-value: #fff; } @property --tw-ring-offset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; } @property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; + syntax: "*"; + inherits: false; + initial-value: solid; } @property --tw-blur { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-brightness { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-contrast { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-grayscale { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-hue-rotate { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-invert { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-opacity { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-saturate { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-sepia { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-drop-shadow { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-drop-shadow-color { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-drop-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; + syntax: ""; + inherits: false; + initial-value: 100%; } @property --tw-drop-shadow-size { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-duration { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @property --tw-ease { - syntax: "*"; - inherits: false; + syntax: "*"; + inherits: false; } @layer properties { - @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { - *, ::before, ::after, ::backdrop { - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; - --tw-space-y-reverse: 0; - --tw-border-style: solid; - --tw-font-weight: initial; - --tw-ordinal: initial; - --tw-slashed-zero: initial; - --tw-numeric-figure: initial; - --tw-numeric-spacing: initial; - --tw-numeric-fraction: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; - --tw-outline-style: solid; - --tw-blur: initial; - --tw-brightness: initial; - --tw-contrast: initial; - --tw-grayscale: initial; - --tw-hue-rotate: initial; - --tw-invert: initial; - --tw-opacity: initial; - --tw-saturate: initial; - --tw-sepia: initial; - --tw-drop-shadow: initial; - --tw-drop-shadow-color: initial; - --tw-drop-shadow-alpha: 100%; - --tw-drop-shadow-size: initial; - --tw-duration: initial; - --tw-ease: initial; - } - } + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or + ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, + ::before, + ::after, + ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-ordinal: initial; + --tw-slashed-zero: initial; + --tw-numeric-figure: initial; + --tw-numeric-spacing: initial; + --tw-numeric-fraction: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + --tw-ease: initial; + } + } } diff --git a/canvas/src/presence/Interfaces/CursorManager.ts b/canvas/src/presence/Interfaces/CursorManager.ts index fc8883a3..1dc8c6ec 100644 --- a/canvas/src/presence/Interfaces/CursorManager.ts +++ b/canvas/src/presence/Interfaces/CursorManager.ts @@ -13,20 +13,10 @@ */ import { PresenceManager } from "./PresenceManager.js"; +import { CursorState } from "../validators.js"; -/** - * Cursor position and visibility data for a single user - */ -export interface CursorState { - /** Canvas-relative X coordinate of the cursor */ - x: number; - /** Canvas-relative Y coordinate of the cursor */ - y: number; - /** Whether the cursor is currently visible (mouse is over canvas) */ - visible: boolean; - /** Timestamp of the last cursor update for staleness detection */ - timestamp: number; -} +// Re-export type for external consumers +export type { CursorState }; /** * CursorManager interface for managing collaborative cursor tracking. diff --git a/canvas/src/presence/Interfaces/DragManager.ts b/canvas/src/presence/Interfaces/DragManager.ts index 8a076a8a..c641b744 100644 --- a/canvas/src/presence/Interfaces/DragManager.ts +++ b/canvas/src/presence/Interfaces/DragManager.ts @@ -13,15 +13,15 @@ */ import { PresenceManager } from "./PresenceManager.js"; +import { DragAndRotatePackage } from "../validators.js"; /** * DragManager interface for managing drag and drop functionality in the collaborative app. * Extends PresenceManager to provide real-time synchronization of drag operations. * - * @template TDragPackage - The type of drag data package, defaults to DragPackage | null + * @template TDragPackage - The type of drag data package */ -export interface DragManager - extends PresenceManager { +export interface DragManager extends PresenceManager { /** * Sets the drag target and position for the current user. * This notifies all connected clients that the current user is dragging an element. @@ -47,16 +47,3 @@ export interface DragManager; - events: Listenable>; - attendees: LatestRaw["presence"]["attendees"]; + state: Latest; + events: Listenable>; + attendees: Latest["presence"]["attendees"]; /** Begin a new ephemeral stroke for the local user. */ setStroke(stroke: EphemeralInkStroke): void; /** Replace the point list of the in‑progress local stroke (cumulative list). */ diff --git a/canvas/src/presence/Interfaces/PresenceManager.ts b/canvas/src/presence/Interfaces/PresenceManager.ts index 1b9e9181..5b65b118 100644 --- a/canvas/src/presence/Interfaces/PresenceManager.ts +++ b/canvas/src/presence/Interfaces/PresenceManager.ts @@ -10,10 +10,10 @@ */ import { - LatestRaw, - LatestRawEvents, - LatestMapRaw, - LatestMapRawEvents, + Latest, + LatestEvents, + LatestMap, + LatestMapEvents, AttendeesEvents, Attendee, AttendeeId, @@ -28,8 +28,8 @@ import { Listenable } from "fluid-framework"; * @template TState - The type of state being managed (e.g., selection, drag, resize) */ export interface PresenceManager { - /** The current state wrapped in Fluid's LatestRaw for real-time sync */ - state: LatestRaw; + /** The current state wrapped in Fluid's Latest for real-time sync with validation */ + state: Latest; /** Interface for managing client connections and attendees * Provides methods to get attendees, their details, and the current user. @@ -42,7 +42,7 @@ export interface PresenceManager { }; /** Event emitter for state change notifications */ - events: Listenable>; + events: Listenable>; } /** @@ -52,8 +52,8 @@ export interface PresenceManager { * @template TState - The type of state being managed */ export interface PresenceMapManager { - /** The current map state wrapped in Fluid's LatestMapRaw for real-time sync */ - state: LatestMapRaw; + /** The current map state wrapped in Fluid's LatestMap for real-time sync with validation */ + state: LatestMap; /** Interface for managing client connections and attendees */ attendees: { @@ -64,5 +64,5 @@ export interface PresenceMapManager { }; /** Event emitter for map state change notifications */ - events: Listenable>; + events: Listenable>; } diff --git a/canvas/src/presence/Interfaces/ResizeManager.ts b/canvas/src/presence/Interfaces/ResizeManager.ts index cb7e7245..166c9d31 100644 --- a/canvas/src/presence/Interfaces/ResizeManager.ts +++ b/canvas/src/presence/Interfaces/ResizeManager.ts @@ -15,6 +15,10 @@ */ import { PresenceManager } from "./PresenceManager.js"; +import { ResizePackage } from "../validators.js"; + +// Re-export type for external consumers +export type { ResizePackage }; /** * ResizeManager interface for managing resize functionality for shapes in the app. @@ -51,18 +55,3 @@ export interface ResizeManager */ getRemoteSelected(): Map; } - -/** - * Base Selection Type - * - * The minimum required structure for any selectable item. All selection objects - * must have a unique 'id' property that identifies them across the application. - * - * Custom selection types can extend this to add additional properties: - * - * @example - * type ShapeSelection = Selection & { - * type: 'circle' | 'rectangle' | 'line'; - * layer: number; - * } - */ -export type Selection = { - /** Unique identifier for the selectable item */ - id: string; -}; diff --git a/canvas/src/presence/Interfaces/UsersManager.ts b/canvas/src/presence/Interfaces/UsersManager.ts index 125b2558..2578f15c 100644 --- a/canvas/src/presence/Interfaces/UsersManager.ts +++ b/canvas/src/presence/Interfaces/UsersManager.ts @@ -16,6 +16,10 @@ import { Attendee } from "@fluidframework/presence/beta"; import { PresenceManager } from "./PresenceManager.js"; +import { UserInfo } from "../validators.js"; + +// Re-export type for external consumers +export type { UserInfo }; /** * UsersManager interface for managing user presence and information in the collaborative app. @@ -105,16 +109,3 @@ export type User = { /** The Fluid Framework client/attendee associated with this user */ client: Attendee; }; - -/** - * UserInfo type definition for basic user profile information. - * This can be extended for additional user properties as needed. - */ -export type UserInfo = { - /** Unique identifier for the user (typically from authentication provider) */ - id: string; - /** Display name of the user */ - name: string; - /** Optional profile picture base 64 encoded*/ - image?: string; -}; diff --git a/canvas/src/presence/cursor.ts b/canvas/src/presence/cursor.ts index 867fab0b..41ba4462 100644 --- a/canvas/src/presence/cursor.ts +++ b/canvas/src/presence/cursor.ts @@ -17,14 +17,10 @@ * over the canvas area. Each user maintains their own cursor state. */ -import { - StateFactory, - StatesWorkspace, - LatestRaw, - LatestRawEvents, -} from "@fluidframework/presence/beta"; +import { StateFactory, StatesWorkspace, Latest, LatestEvents } from "@fluidframework/presence/beta"; import { Listenable } from "fluid-framework"; -import { CursorManager, CursorState } from "./Interfaces/CursorManager.js"; +import { CursorManager } from "./Interfaces/CursorManager.js"; +import { CursorState, validateCursorState } from "./validators.js"; /** * Creates a new CursorManager instance with the given workspace. @@ -48,18 +44,25 @@ export function createCursorManager(props: { */ class CursorManagerImpl implements CursorManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the cursor manager with Fluid Framework state management. - * Sets up the latest state factory and registers with the workspace. + * Sets up the latest state factory with validation and registers with the workspace. * * @param name - Unique identifier for this cursor manager * @param workspace - Fluid workspace for state synchronization */ constructor(name: string, workspace: StatesWorkspace<{}>) { // Register this cursor manager's state with the Fluid workspace - workspace.add(name, StateFactory.latest({ local: null })); + // Using validated Latest state to ensure data integrity + workspace.add( + name, + StateFactory.latest({ + local: null, + validator: validateCursorState, + }) + ); this.state = workspace.states[name]; } @@ -75,7 +78,7 @@ export function createCursorManager(props: { * Event emitter for cursor state changes. * Components can subscribe to these events to update their UI when cursor positions change. */ - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } @@ -146,7 +149,7 @@ export function createCursorManager(props: { // Get all remote cursor states const remotes = this.state.getRemotes(); for (const remote of remotes) { - const cursorState = remote.value; + const cursorState = remote.value(); // Only include visible, non-stale cursors from connected clients if ( cursorState && diff --git a/canvas/src/presence/drag.ts b/canvas/src/presence/drag.ts index cf1c6d32..52c0b36d 100644 --- a/canvas/src/presence/drag.ts +++ b/canvas/src/presence/drag.ts @@ -18,14 +18,13 @@ * the current position and rotation of the dragged item. */ -import { - StateFactory, - StatesWorkspace, - LatestRaw, - LatestRawEvents, -} from "@fluidframework/presence/beta"; +import { StateFactory, StatesWorkspace, Latest, LatestEvents } from "@fluidframework/presence/beta"; import { Listenable } from "fluid-framework"; -import { DragManager, DragPackage } from "./Interfaces/DragManager.js"; +import { DragManager } from "./Interfaces/DragManager.js"; +import { DragAndRotatePackage, validateDragAndRotatePackage } from "./validators.js"; + +// Re-export types for external consumers +export type { DragAndRotatePackage }; /** * Creates a new DragManager instance with the given presence and workspace. @@ -49,18 +48,25 @@ export function createDragManager(props: { */ class DragManagerImpl implements DragManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the drag manager with Fluid Framework state management. - * Sets up the latest state factory and registers with the workspace. + * Sets up the latest state factory with validation and registers with the workspace. * * @param name - Unique identifier for this drag manager * @param workspace - Fluid workspace for state synchronization */ constructor(name: string, workspace: StatesWorkspace<{}>) { // Register this drag manager's state with the Fluid workspace - workspace.add(name, StateFactory.latest({ local: null })); + // Using validated Latest state to ensure data integrity + workspace.add( + name, + StateFactory.latest({ + local: null, + validator: validateDragAndRotatePackage, + }) + ); this.state = workspace.states[name]; } @@ -76,7 +82,7 @@ export function createDragManager(props: { * Event emitter for drag state changes. * Components can subscribe to these events to update their UI when drag operations occur. */ - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } @@ -102,14 +108,3 @@ export function createDragManager(props: { return new DragManagerImpl(name, workspace); } - -/** - * Extended drag package type that includes rotation and branch information. - * This is more comprehensive than the base DragPackage to support the demo's features. - */ -export interface DragAndRotatePackage extends DragPackage { - /** Current rotation angle of the dragged element in degrees */ - rotation: number; - /** Whether this is a branch drag operation (for tree structures) */ - branch: boolean; -} diff --git a/canvas/src/presence/ink.ts b/canvas/src/presence/ink.ts index 59321b52..b00a6ec3 100644 --- a/canvas/src/presence/ink.ts +++ b/canvas/src/presence/ink.ts @@ -1,11 +1,7 @@ -import { - StateFactory, - StatesWorkspace, - LatestRaw, - LatestRawEvents, -} from "@fluidframework/presence/beta"; +import { StateFactory, StatesWorkspace, Latest, LatestEvents } from "@fluidframework/presence/beta"; import { Listenable } from "fluid-framework"; -import { InkPresenceManager, EphemeralInkStroke, EphemeralPoint } from "./Interfaces/InkManager.js"; +import { InkPresenceManager } from "./Interfaces/InkManager.js"; +import { EphemeralInkStroke, EphemeralPoint, validateEphemeralInkStroke } from "./validators.js"; /* eslint-disable @typescript-eslint/no-empty-object-type */ export function createInkPresenceManager(props: { @@ -15,12 +11,18 @@ export function createInkPresenceManager(props: { const { workspace, name } = props; class InkPresenceManagerImpl implements InkPresenceManager { - state: LatestRaw; + state: Latest; constructor(name: string, workspace: StatesWorkspace<{}>) { - workspace.add(name, StateFactory.latest({ local: null })); + workspace.add( + name, + StateFactory.latest({ + local: null, + validator: validateEphemeralInkStroke, + }) + ); this.state = workspace.states[name]; } - get events(): Listenable> { + get events(): Listenable> { return this.state.events; } get attendees() { @@ -30,8 +32,9 @@ export function createInkPresenceManager(props: { this.state.local = stroke; } updateStroke(points: EphemeralPoint[]) { - if (this.state.local) { - this.state.local = { ...this.state.local, points }; + const currentStroke = this.state.local; + if (currentStroke) { + this.state.local = { ...currentStroke, points }; } } clearStroke() { @@ -40,8 +43,13 @@ export function createInkPresenceManager(props: { getRemoteStrokes() { const out: { stroke: EphemeralInkStroke; attendeeId: string }[] = []; for (const cv of this.state.getRemotes()) { - if (cv.value) { - out.push({ stroke: cv.value, attendeeId: cv.attendee.attendeeId }); + const strokeValue = cv.value(); + if (strokeValue) { + // Cast to mutable type for compatibility + out.push({ + stroke: strokeValue as EphemeralInkStroke, + attendeeId: cv.attendee.attendeeId, + }); } } return out; diff --git a/canvas/src/presence/resize.ts b/canvas/src/presence/resize.ts index ed18966c..f505474c 100644 --- a/canvas/src/presence/resize.ts +++ b/canvas/src/presence/resize.ts @@ -19,14 +19,10 @@ * the current position and size of the element being resized. */ -import { - StateFactory, - StatesWorkspace, - LatestRaw, - LatestRawEvents, -} from "@fluidframework/presence/beta"; +import { StateFactory, StatesWorkspace, Latest, LatestEvents } from "@fluidframework/presence/beta"; import { Listenable } from "fluid-framework"; -import { ResizeManager, ResizePackage } from "./Interfaces/ResizeManager.js"; +import { ResizeManager } from "./Interfaces/ResizeManager.js"; +import { ResizePackage, validateResizePackage } from "./validators.js"; /** * Creates a new ResizeManager instance with the given workspace configuration. @@ -50,18 +46,25 @@ export function createResizeManager(props: { */ class ResizeManagerImpl implements ResizeManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the resize manager with Fluid Framework state management. - * Sets up the latest state factory and registers with the workspace. + * Sets up the latest state factory with validation and registers with the workspace. * * @param name - Unique identifier for this resize manager * @param workspace - Fluid workspace for state synchronization */ constructor(name: string, workspace: StatesWorkspace<{}>) { // Register this resize manager's state with the Fluid workspace - workspace.add(name, StateFactory.latest({ local: null })); + // Using validated Latest state to ensure data integrity + workspace.add( + name, + StateFactory.latest({ + local: null, + validator: validateResizePackage, + }) + ); this.state = workspace.states[name]; } @@ -77,7 +80,7 @@ export function createResizeManager(props: { * Event emitter for resize state changes. * Components can subscribe to these events to update their UI when resize operations occur. */ - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } diff --git a/canvas/src/presence/selection.ts b/canvas/src/presence/selection.ts index d8d6b3bb..cd779ccc 100644 --- a/canvas/src/presence/selection.ts +++ b/canvas/src/presence/selection.ts @@ -25,14 +25,19 @@ * smooth collaborative editing experiences. */ -import { - StateFactory, - LatestRawEvents, - StatesWorkspace, - LatestRaw, -} from "@fluidframework/presence/beta"; +import { StateFactory, LatestEvents, StatesWorkspace, Latest } from "@fluidframework/presence/beta"; import { Listenable } from "fluid-framework"; -import { SelectionManager, Selection } from "./Interfaces/SelectionManager.js"; +import { SelectionManager } from "./Interfaces/SelectionManager.js"; +import { + Selection, + TypedSelection, + selectionType, + validateSelectionArray, + validateTypedSelectionArray, +} from "./validators.js"; + +// Re-export types for external consumers +export type { TypedSelection, selectionType }; /** * Creates a new SelectionManager instance for managing collaborative selections. @@ -56,18 +61,22 @@ export function createSelectionManager(props: { */ class SelectionManagerImpl implements SelectionManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the selection manager with Fluid Framework state management. - * Sets up the latest state factory and registers with the workspace. + * Sets up the latest state factory with validation and registers with the workspace. * * @param name - Unique identifier for this selection manager * @param workspace - Fluid workspace for state synchronization */ constructor(name: string, workspace: StatesWorkspace<{}>) { // Register this selection manager's state with the Fluid workspace - workspace.add(name, StateFactory.latest({ local: [] })); + // Using validated Latest state to ensure data integrity + workspace.add( + name, + StateFactory.latest({ local: [], validator: validateSelectionArray }) + ); this.state = workspace.states[name]; } @@ -75,7 +84,7 @@ export function createSelectionManager(props: { * Event emitter for selection state changes. * Components can subscribe to these events to update their UI when selections change. */ - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } @@ -108,7 +117,8 @@ export function createSelectionManager(props: { const remoteSelectedClients: string[] = []; for (const cv of this.state.getRemotes()) { if (cv.attendee.getConnectionStatus() === "Connected") { - if (this._testForInclusion(sel, cv.value)) { + const remoteValue = cv.value(); + if (remoteValue && this._testForInclusion(sel, remoteValue)) { remoteSelectedClients.push(cv.attendee.attendeeId); } } @@ -177,7 +187,7 @@ export function createSelectionManager(props: { * @param sel - The selection to remove */ public removeFromSelection(sel: Selection) { - const arr: Selection[] = this.state.local.filter((s) => s.id !== sel.id); + const arr: Selection[] = this.state.local.filter((s: Selection) => s.id !== sel.id); this.state.local = arr; } @@ -202,11 +212,14 @@ export function createSelectionManager(props: { const remoteSelected = new Map(); for (const cv of this.state.getRemotes()) { if (cv.attendee.getConnectionStatus() === "Connected") { - for (const sel of cv.value) { - if (!remoteSelected.has(sel)) { - remoteSelected.set(sel, []); + const remoteValue = cv.value(); + if (remoteValue) { + for (const sel of remoteValue) { + if (!remoteSelected.has(sel)) { + remoteSelected.set(sel, []); + } + remoteSelected.get(sel)?.push(cv.attendee.attendeeId); } - remoteSelected.get(sel)?.push(cv.attendee.attendeeId); } } } @@ -249,17 +262,23 @@ export function createTypedSelectionManager(props: { */ class TypedSelectionManagerImpl implements SelectionManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the selection manager with Fluid Framework state management. */ constructor(name: string, workspace: StatesWorkspace<{}>) { - workspace.add(name, StateFactory.latest({ local: [] })); + workspace.add( + name, + StateFactory.latest({ + local: [], + validator: validateTypedSelectionArray, + }) + ); this.state = workspace.states[name]; } - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } @@ -275,7 +294,8 @@ export function createTypedSelectionManager(props: { const remoteSelectedClients: string[] = []; for (const cv of this.state.getRemotes()) { if (cv.attendee.getConnectionStatus() === "Connected") { - if (this._testForInclusion(sel, cv.value)) { + const remoteValue = cv.value(); + if (remoteValue && this._testForInclusion(sel, remoteValue)) { remoteSelectedClients.push(cv.attendee.attendeeId); } } @@ -312,7 +332,9 @@ export function createTypedSelectionManager(props: { } public removeFromSelection(sel: TypedSelection) { - const arr: TypedSelection[] = this.state.local.filter((s) => s.id !== sel.id); + const arr: TypedSelection[] = this.state.local.filter( + (s: TypedSelection) => s.id !== sel.id + ); this.state.local = arr; } @@ -324,11 +346,14 @@ export function createTypedSelectionManager(props: { const remoteSelected = new Map(); for (const cv of this.state.getRemotes()) { if (cv.attendee.getConnectionStatus() === "Connected") { - for (const sel of cv.value) { - if (!remoteSelected.has(sel)) { - remoteSelected.set(sel, []); + const remoteValue = cv.value(); + if (remoteValue) { + for (const sel of remoteValue) { + if (!remoteSelected.has(sel)) { + remoteSelected.set(sel, []); + } + remoteSelected.get(sel)?.push(cv.attendee.attendeeId); } - remoteSelected.get(sel)?.push(cv.attendee.attendeeId); } } } @@ -345,20 +370,3 @@ export function createTypedSelectionManager(props: { return new TypedSelectionManagerImpl(name, workspace); } - -/** - * TypedSelection type definition with optional type information. - * Extends basic selection with type metadata for enhanced functionality. - */ -export type TypedSelection = { - /** Unique identifier for the selected item */ - id: string; - /** Optional type of the selection (row, column, cell, etc.) */ - type?: selectionType; -}; - -/** - * Enumeration of supported selection types. - * Used for providing context about what kind of element is selected. - */ -export type selectionType = "row" | "column" | "cell"; diff --git a/canvas/src/presence/users.ts b/canvas/src/presence/users.ts index be4a31ce..345ef3e1 100644 --- a/canvas/src/presence/users.ts +++ b/canvas/src/presence/users.ts @@ -21,13 +21,14 @@ import { StateFactory, - LatestRawEvents, + LatestEvents, StatesWorkspace, - LatestRaw, + Latest, AttendeeStatus, } from "@fluidframework/presence/beta"; -import { UsersManager, User, UserInfo } from "./Interfaces/UsersManager.js"; +import { UsersManager, User } from "./Interfaces/UsersManager.js"; import { Listenable } from "fluid-framework"; +import { UserInfo, validateUserInfo } from "./validators.js"; /** * Creates a new UsersManager instance with the given workspace configuration @@ -52,18 +53,19 @@ export function createUsersManager(props: { */ class UsersManagerImpl implements UsersManager { /** Fluid Framework state object for real-time synchronization */ - state: LatestRaw; + state: Latest; /** * Initializes the users manager with Fluid Framework state management. - * Sets up the latest state factory and registers with the workspace. + * Sets up the latest state factory with validation and registers with the workspace. * * @param name - Unique identifier for this users manager * @param workspace - Fluid workspace for state synchronization */ constructor(name: string, workspace: StatesWorkspace<{}>) { // Register this users manager's state with the Fluid workspace - workspace.add(name, StateFactory.latest({ local: me })); + // Using validated Latest state to ensure data integrity + workspace.add(name, StateFactory.latest({ local: me, validator: validateUserInfo })); this.state = workspace.states[name]; } @@ -71,7 +73,7 @@ export function createUsersManager(props: { * Event emitter for user information changes. * Components can subscribe to these events to update their UI when user data changes. */ - public get events(): Listenable> { + public get events(): Listenable> { return this.state.events; } @@ -90,7 +92,10 @@ export function createUsersManager(props: { * @returns Read-only array of all users with their profile and client data */ getUsers(): readonly User[] { - return [...this.state.getRemotes()].map((c) => ({ ...c, client: c.attendee })); + return [...this.state.getRemotes()].map((c) => ({ + value: c.value() as UserInfo, + client: c.attendee, + })); } /** @@ -134,7 +139,10 @@ export function createUsersManager(props: { * @returns User object for the current user (myself) */ getMyself(): User { - return { value: this.state.local, client: this.state.presence.attendees.getMyself() }; + return { + value: this.state.local, + client: this.state.presence.attendees.getMyself(), + }; } } diff --git a/canvas/src/presence/validators.ts b/canvas/src/presence/validators.ts new file mode 100644 index 00000000..8aef1c53 --- /dev/null +++ b/canvas/src/presence/validators.ts @@ -0,0 +1,200 @@ +/** + * Presence Data Validators + * + * This module provides runtime validation for all Fluid Framework presence data types + * using Zod schemas. The Fluid Framework presence API supports runtime validation to + * prevent type errors and ensure data integrity across connected clients. + * + * These validators are used with StateFactory.latest() to create validated presence states + * that automatically check incoming data against the defined schemas. + */ + +import { z } from "zod"; + +/** + * UserInfo Schema + * Validates user profile information including ID, name, and optional avatar image. + */ +export const UserInfoSchema = z.object({ + id: z.string(), + name: z.string(), + image: z.string().optional(), +}); + +export type UserInfo = z.infer; + +/** + * UserInfo Validator Function + * Converts unknown data to UserInfo or returns undefined if validation fails. + */ +export function validateUserInfo(value: unknown): UserInfo | undefined { + const result = UserInfoSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * Selection Schema + * Validates basic selection objects with a unique ID. + */ +export const SelectionSchema = z.object({ + id: z.string(), +}); + +export type Selection = z.infer; + +/** + * Selection Validator Function + */ +export function validateSelection(value: unknown): Selection | undefined { + const result = SelectionSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * TypedSelection Schema + * Extends Selection with optional type information for table selections. + */ +export const TypedSelectionSchema = z.object({ + id: z.string(), + type: z.enum(["row", "column", "cell"]).optional(), +}); + +export type TypedSelection = z.infer; + +/** + * Enumeration of supported selection types. + * Used for providing context about what kind of element is selected. + */ +export type selectionType = "row" | "column" | "cell"; + +/** + * TypedSelection Validator Function + */ +export function validateTypedSelection(value: unknown): TypedSelection | undefined { + const result = TypedSelectionSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * Selection Array Validator Function + */ +export function validateSelectionArray(value: unknown): Selection[] | undefined { + const schema = z.array(SelectionSchema); + const result = schema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * TypedSelection Array Validator Function + */ +export function validateTypedSelectionArray(value: unknown): TypedSelection[] | undefined { + const schema = z.array(TypedSelectionSchema); + const result = schema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * DragAndRotatePackage Schema + * Validates drag operation data including position, rotation, and branch info. + */ +export const DragAndRotatePackageSchema = z.object({ + id: z.string(), + x: z.number(), + y: z.number(), + rotation: z.number(), + branch: z.boolean(), +}); + +export type DragAndRotatePackage = z.infer; + +/** + * DragAndRotatePackage Validator Function + */ +export function validateDragAndRotatePackage( + value: unknown +): DragAndRotatePackage | null | undefined { + if (value === null) return null; + const result = DragAndRotatePackageSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * ResizePackage Schema + * Validates resize operation data including position and size. + */ +export const ResizePackageSchema = z.object({ + id: z.string(), + x: z.number(), + y: z.number(), + size: z.number(), +}); + +export type ResizePackage = z.infer; + +/** + * ResizePackage Validator Function + */ +export function validateResizePackage(value: unknown): ResizePackage | null | undefined { + if (value === null) return null; + const result = ResizePackageSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * EphemeralPoint Schema + * Validates a single point in an ephemeral ink stroke. + */ +export const EphemeralPointSchema = z.object({ + x: z.number(), + y: z.number(), + t: z.number().optional(), + p: z.number().optional(), +}); + +export type EphemeralPoint = z.infer; + +/** + * EphemeralInkStroke Schema + * Validates ephemeral ink stroke data for real-time drawing. + */ +export const EphemeralInkStrokeSchema = z.object({ + id: z.string(), + points: z.array(EphemeralPointSchema), + color: z.string(), + width: z.number(), + opacity: z.number(), + startTime: z.number(), +}); + +export type EphemeralInkStroke = z.infer; + +/** + * EphemeralInkStroke Validator Function + */ +export function validateEphemeralInkStroke(value: unknown): EphemeralInkStroke | null | undefined { + if (value === null) return null; + const result = EphemeralInkStrokeSchema.safeParse(value); + return result.success ? result.data : undefined; +} + +/** + * CursorState Schema + * Validates cursor position and visibility state. + */ +export const CursorStateSchema = z.object({ + x: z.number(), + y: z.number(), + visible: z.boolean(), + timestamp: z.number(), +}); + +export type CursorState = z.infer; + +/** + * CursorState Validator Function + */ +export function validateCursorState(value: unknown): CursorState | null | undefined { + if (value === null) return null; + const result = CursorStateSchema.safeParse(value); + return result.success ? result.data : undefined; +} From d34341d9670c31ef70df969c4931d92f2ea25d32 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 13 Oct 2025 10:22:55 -0700 Subject: [PATCH 2/2] lint --- canvas/src/presence/Interfaces/DragManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/canvas/src/presence/Interfaces/DragManager.ts b/canvas/src/presence/Interfaces/DragManager.ts index c641b744..f4ebae9a 100644 --- a/canvas/src/presence/Interfaces/DragManager.ts +++ b/canvas/src/presence/Interfaces/DragManager.ts @@ -13,7 +13,6 @@ */ import { PresenceManager } from "./PresenceManager.js"; -import { DragAndRotatePackage } from "../validators.js"; /** * DragManager interface for managing drag and drop functionality in the collaborative app.