From 7e00d6fcd3d7ec081aca651bd1fd43885ddb0be9 Mon Sep 17 00:00:00 2001 From: Malik Alleyne-Jones Date: Sat, 16 May 2026 21:51:30 -0400 Subject: [PATCH 1/2] feat: add an opt-in star rating to comments (#29) Lets a user attach a star rating to a comment, review-style. - New nullable `rating` column via an additive migration; the Comment model casts it to an integer. - Opt-in: a `commentions.ratings` config block (disabled by default) plus CommentsEntry::make()->enableRatings() / ->maxRating(), threaded through the entry and actions to the Comments component. - The Comments editor shows an interactive star input when enabled; the rating is validated (1..max) and persisted via SaveComment. - Each comment renders its rating as filled/empty stars. --- config/commentions.php | 16 ++++ ...ing_to_commentions_comments_table.php.stub | 22 +++++ resources/css/commentions.css | 9 ++ resources/dist/commentions.css | 2 +- resources/views/comment.blade.php | 15 +++ resources/views/comments-modal.blade.php | 2 + resources/views/comments.blade.php | 22 +++++ .../components/comments-entry.blade.php | 2 + src/Actions/SaveComment.php | 12 ++- src/Comment.php | 6 ++ src/CommentionsServiceProvider.php | 1 + src/Config.php | 5 + src/Filament/Actions/CommentsAction.php | 4 + src/Filament/Actions/CommentsTableAction.php | 4 + src/Filament/Concerns/HasRatings.php | 45 +++++++++ .../Infolists/Components/CommentsEntry.php | 2 + src/Livewire/Comments.php | 26 ++++- tests/Livewire/CommentRatingTest.php | 95 +++++++++++++++++++ tests/TestCase.php | 1 + 19 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 database/migrations/add_rating_to_commentions_comments_table.php.stub create mode 100644 src/Filament/Concerns/HasRatings.php create mode 100644 tests/Livewire/CommentRatingTest.php diff --git a/config/commentions.php b/config/commentions.php index 4267451..0228b72 100644 --- a/config/commentions.php +++ b/config/commentions.php @@ -46,6 +46,22 @@ 'allowed' => ['👍', '❤️', '😂', '😮', '😢', '🤔'], ], + /* + |-------------------------------------------------------------------------- + | Ratings + |-------------------------------------------------------------------------- + | + | Allow a star rating to be attached to a comment. Disabled by default; + | enable globally here or per component with + | CommentsEntry::make()->enableRatings(). + | + */ + 'ratings' => [ + 'enabled' => env('COMMENTIONS_RATINGS_ENABLED', false), + + 'max' => (int) env('COMMENTIONS_RATINGS_MAX', 5), + ], + /* |-------------------------------------------------------------------------- | Subscriptions diff --git a/database/migrations/add_rating_to_commentions_comments_table.php.stub b/database/migrations/add_rating_to_commentions_comments_table.php.stub new file mode 100644 index 0000000..5b80008 --- /dev/null +++ b/database/migrations/add_rating_to_commentions_comments_table.php.stub @@ -0,0 +1,22 @@ +unsignedTinyInteger('rating')->nullable()->after('body'); + }); + } + + public function down(): void + { + Schema::table(config('commentions.tables.comments', 'comments'), function (Blueprint $table) { + $table->dropColumn('rating'); + }); + } +}; diff --git a/resources/css/commentions.css b/resources/css/commentions.css index 695c00d..7652274 100644 --- a/resources/css/commentions.css +++ b/resources/css/commentions.css @@ -44,3 +44,12 @@ .mention-item:hover { @apply comm:bg-gray-100; } + +.commentions-rating-star { + @apply comm:flex comm:cursor-pointer comm:items-center comm:justify-center comm:text-gray-300 comm:dark:text-gray-600; +} + +.commentions-rating-star:hover, +.commentions-rating-star-active { + @apply comm:text-amber-400; +} diff --git a/resources/dist/commentions.css b/resources/dist/commentions.css index 1807ccf..e23cb4b 100644 --- a/resources/dist/commentions.css +++ b/resources/dist/commentions.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.0.6 | MIT License | https://tailwindcss.com */ -@layer theme{:root,:host{--comm-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--comm-font-serif:ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--comm-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--comm-color-red-50:oklch(.971 .013 17.38);--comm-color-red-100:oklch(.936 .032 17.717);--comm-color-red-200:oklch(.885 .062 18.334);--comm-color-red-300:oklch(.808 .114 19.571);--comm-color-red-400:oklch(.704 .191 22.216);--comm-color-red-500:oklch(.637 .237 25.331);--comm-color-red-600:oklch(.577 .245 27.325);--comm-color-red-700:oklch(.505 .213 27.518);--comm-color-red-800:oklch(.444 .177 26.899);--comm-color-red-900:oklch(.396 .141 25.723);--comm-color-red-950:oklch(.258 .092 26.042);--comm-color-orange-50:oklch(.98 .016 73.684);--comm-color-orange-100:oklch(.954 .038 75.164);--comm-color-orange-200:oklch(.901 .076 70.697);--comm-color-orange-300:oklch(.837 .128 66.29);--comm-color-orange-400:oklch(.75 .183 55.934);--comm-color-orange-500:oklch(.705 .213 47.604);--comm-color-orange-600:oklch(.646 .222 41.116);--comm-color-orange-700:oklch(.553 .195 38.402);--comm-color-orange-800:oklch(.47 .157 37.304);--comm-color-orange-900:oklch(.408 .123 38.172);--comm-color-orange-950:oklch(.266 .079 36.259);--comm-color-amber-50:oklch(.987 .022 95.277);--comm-color-amber-100:oklch(.962 .059 95.617);--comm-color-amber-200:oklch(.924 .12 95.746);--comm-color-amber-300:oklch(.879 .169 91.605);--comm-color-amber-400:oklch(.828 .189 84.429);--comm-color-amber-500:oklch(.769 .188 70.08);--comm-color-amber-600:oklch(.666 .179 58.318);--comm-color-amber-700:oklch(.555 .163 48.998);--comm-color-amber-800:oklch(.473 .137 46.201);--comm-color-amber-900:oklch(.414 .112 45.904);--comm-color-amber-950:oklch(.279 .077 45.635);--comm-color-yellow-50:oklch(.987 .026 102.212);--comm-color-yellow-100:oklch(.973 .071 103.193);--comm-color-yellow-200:oklch(.945 .129 101.54);--comm-color-yellow-300:oklch(.905 .182 98.111);--comm-color-yellow-400:oklch(.852 .199 91.936);--comm-color-yellow-500:oklch(.795 .184 86.047);--comm-color-yellow-600:oklch(.681 .162 75.834);--comm-color-yellow-700:oklch(.554 .135 66.442);--comm-color-yellow-800:oklch(.476 .114 61.907);--comm-color-yellow-900:oklch(.421 .095 57.708);--comm-color-yellow-950:oklch(.286 .066 53.813);--comm-color-lime-50:oklch(.986 .031 120.757);--comm-color-lime-100:oklch(.967 .067 122.328);--comm-color-lime-200:oklch(.938 .127 124.321);--comm-color-lime-300:oklch(.897 .196 126.665);--comm-color-lime-400:oklch(.841 .238 128.85);--comm-color-lime-500:oklch(.768 .233 130.85);--comm-color-lime-600:oklch(.648 .2 131.684);--comm-color-lime-700:oklch(.532 .157 131.589);--comm-color-lime-800:oklch(.453 .124 130.933);--comm-color-lime-900:oklch(.405 .101 131.063);--comm-color-lime-950:oklch(.274 .072 132.109);--comm-color-green-50:oklch(.982 .018 155.826);--comm-color-green-100:oklch(.962 .044 156.743);--comm-color-green-200:oklch(.925 .084 155.995);--comm-color-green-300:oklch(.871 .15 154.449);--comm-color-green-400:oklch(.792 .209 151.711);--comm-color-green-500:oklch(.723 .219 149.579);--comm-color-green-600:oklch(.627 .194 149.214);--comm-color-green-700:oklch(.527 .154 150.069);--comm-color-green-800:oklch(.448 .119 151.328);--comm-color-green-900:oklch(.393 .095 152.535);--comm-color-green-950:oklch(.266 .065 152.934);--comm-color-emerald-50:oklch(.979 .021 166.113);--comm-color-emerald-100:oklch(.95 .052 163.051);--comm-color-emerald-200:oklch(.905 .093 164.15);--comm-color-emerald-300:oklch(.845 .143 164.978);--comm-color-emerald-400:oklch(.765 .177 163.223);--comm-color-emerald-500:oklch(.696 .17 162.48);--comm-color-emerald-600:oklch(.596 .145 163.225);--comm-color-emerald-700:oklch(.508 .118 165.612);--comm-color-emerald-800:oklch(.432 .095 166.913);--comm-color-emerald-900:oklch(.378 .077 168.94);--comm-color-emerald-950:oklch(.262 .051 172.552);--comm-color-teal-50:oklch(.984 .014 180.72);--comm-color-teal-100:oklch(.953 .051 180.801);--comm-color-teal-200:oklch(.91 .096 180.426);--comm-color-teal-300:oklch(.855 .138 181.071);--comm-color-teal-400:oklch(.777 .152 181.912);--comm-color-teal-500:oklch(.704 .14 182.503);--comm-color-teal-600:oklch(.6 .118 184.704);--comm-color-teal-700:oklch(.511 .096 186.391);--comm-color-teal-800:oklch(.437 .078 188.216);--comm-color-teal-900:oklch(.386 .063 188.416);--comm-color-teal-950:oklch(.277 .046 192.524);--comm-color-cyan-50:oklch(.984 .019 200.873);--comm-color-cyan-100:oklch(.956 .045 203.388);--comm-color-cyan-200:oklch(.917 .08 205.041);--comm-color-cyan-300:oklch(.865 .127 207.078);--comm-color-cyan-400:oklch(.789 .154 211.53);--comm-color-cyan-500:oklch(.715 .143 215.221);--comm-color-cyan-600:oklch(.609 .126 221.723);--comm-color-cyan-700:oklch(.52 .105 223.128);--comm-color-cyan-800:oklch(.45 .085 224.283);--comm-color-cyan-900:oklch(.398 .07 227.392);--comm-color-cyan-950:oklch(.302 .056 229.695);--comm-color-sky-50:oklch(.977 .013 236.62);--comm-color-sky-100:oklch(.951 .026 236.824);--comm-color-sky-200:oklch(.901 .058 230.902);--comm-color-sky-300:oklch(.828 .111 230.318);--comm-color-sky-400:oklch(.746 .16 232.661);--comm-color-sky-500:oklch(.685 .169 237.323);--comm-color-sky-600:oklch(.588 .158 241.966);--comm-color-sky-700:oklch(.5 .134 242.749);--comm-color-sky-800:oklch(.443 .11 240.79);--comm-color-sky-900:oklch(.391 .09 240.876);--comm-color-sky-950:oklch(.293 .066 243.157);--comm-color-blue-50:oklch(.97 .014 254.604);--comm-color-blue-100:oklch(.932 .032 255.585);--comm-color-blue-200:oklch(.882 .059 254.128);--comm-color-blue-300:oklch(.809 .105 251.813);--comm-color-blue-400:oklch(.707 .165 254.624);--comm-color-blue-500:oklch(.623 .214 259.815);--comm-color-blue-600:oklch(.546 .245 262.881);--comm-color-blue-700:oklch(.488 .243 264.376);--comm-color-blue-800:oklch(.424 .199 265.638);--comm-color-blue-900:oklch(.379 .146 265.522);--comm-color-blue-950:oklch(.282 .091 267.935);--comm-color-indigo-50:oklch(.962 .018 272.314);--comm-color-indigo-100:oklch(.93 .034 272.788);--comm-color-indigo-200:oklch(.87 .065 274.039);--comm-color-indigo-300:oklch(.785 .115 274.713);--comm-color-indigo-400:oklch(.673 .182 276.935);--comm-color-indigo-500:oklch(.585 .233 277.117);--comm-color-indigo-600:oklch(.511 .262 276.966);--comm-color-indigo-700:oklch(.457 .24 277.023);--comm-color-indigo-800:oklch(.398 .195 277.366);--comm-color-indigo-900:oklch(.359 .144 278.697);--comm-color-indigo-950:oklch(.257 .09 281.288);--comm-color-violet-50:oklch(.969 .016 293.756);--comm-color-violet-100:oklch(.943 .029 294.588);--comm-color-violet-200:oklch(.894 .057 293.283);--comm-color-violet-300:oklch(.811 .111 293.571);--comm-color-violet-400:oklch(.702 .183 293.541);--comm-color-violet-500:oklch(.606 .25 292.717);--comm-color-violet-600:oklch(.541 .281 293.009);--comm-color-violet-700:oklch(.491 .27 292.581);--comm-color-violet-800:oklch(.432 .232 292.759);--comm-color-violet-900:oklch(.38 .189 293.745);--comm-color-violet-950:oklch(.283 .141 291.089);--comm-color-purple-50:oklch(.977 .014 308.299);--comm-color-purple-100:oklch(.946 .033 307.174);--comm-color-purple-200:oklch(.902 .063 306.703);--comm-color-purple-300:oklch(.827 .119 306.383);--comm-color-purple-400:oklch(.714 .203 305.504);--comm-color-purple-500:oklch(.627 .265 303.9);--comm-color-purple-600:oklch(.558 .288 302.321);--comm-color-purple-700:oklch(.496 .265 301.924);--comm-color-purple-800:oklch(.438 .218 303.724);--comm-color-purple-900:oklch(.381 .176 304.987);--comm-color-purple-950:oklch(.291 .149 302.717);--comm-color-fuchsia-50:oklch(.977 .017 320.058);--comm-color-fuchsia-100:oklch(.952 .037 318.852);--comm-color-fuchsia-200:oklch(.903 .076 319.62);--comm-color-fuchsia-300:oklch(.833 .145 321.434);--comm-color-fuchsia-400:oklch(.74 .238 322.16);--comm-color-fuchsia-500:oklch(.667 .295 322.15);--comm-color-fuchsia-600:oklch(.591 .293 322.896);--comm-color-fuchsia-700:oklch(.518 .253 323.949);--comm-color-fuchsia-800:oklch(.452 .211 324.591);--comm-color-fuchsia-900:oklch(.401 .17 325.612);--comm-color-fuchsia-950:oklch(.293 .136 325.661);--comm-color-pink-50:oklch(.971 .014 343.198);--comm-color-pink-100:oklch(.948 .028 342.258);--comm-color-pink-200:oklch(.899 .061 343.231);--comm-color-pink-300:oklch(.823 .12 346.018);--comm-color-pink-400:oklch(.718 .202 349.761);--comm-color-pink-500:oklch(.656 .241 354.308);--comm-color-pink-600:oklch(.592 .249 .584);--comm-color-pink-700:oklch(.525 .223 3.958);--comm-color-pink-800:oklch(.459 .187 3.815);--comm-color-pink-900:oklch(.408 .153 2.432);--comm-color-pink-950:oklch(.284 .109 3.907);--comm-color-rose-50:oklch(.969 .015 12.422);--comm-color-rose-100:oklch(.941 .03 12.58);--comm-color-rose-200:oklch(.892 .058 10.001);--comm-color-rose-300:oklch(.81 .117 11.638);--comm-color-rose-400:oklch(.712 .194 13.428);--comm-color-rose-500:oklch(.645 .246 16.439);--comm-color-rose-600:oklch(.586 .253 17.585);--comm-color-rose-700:oklch(.514 .222 16.935);--comm-color-rose-800:oklch(.455 .188 13.697);--comm-color-rose-900:oklch(.41 .159 10.272);--comm-color-rose-950:oklch(.271 .105 12.094);--comm-color-slate-50:oklch(.984 .003 247.858);--comm-color-slate-100:oklch(.968 .007 247.896);--comm-color-slate-200:oklch(.929 .013 255.508);--comm-color-slate-300:oklch(.869 .022 252.894);--comm-color-slate-400:oklch(.704 .04 256.788);--comm-color-slate-500:oklch(.554 .046 257.417);--comm-color-slate-600:oklch(.446 .043 257.281);--comm-color-slate-700:oklch(.372 .044 257.287);--comm-color-slate-800:oklch(.279 .041 260.031);--comm-color-slate-900:oklch(.208 .042 265.755);--comm-color-slate-950:oklch(.129 .042 264.695);--comm-color-gray-50:oklch(.985 .002 247.839);--comm-color-gray-100:oklch(.967 .003 264.542);--comm-color-gray-200:oklch(.928 .006 264.531);--comm-color-gray-300:oklch(.872 .01 258.338);--comm-color-gray-400:oklch(.707 .022 261.325);--comm-color-gray-500:oklch(.551 .027 264.364);--comm-color-gray-600:oklch(.446 .03 256.802);--comm-color-gray-700:oklch(.373 .034 259.733);--comm-color-gray-800:oklch(.278 .033 256.848);--comm-color-gray-900:oklch(.21 .034 264.665);--comm-color-gray-950:oklch(.13 .028 261.692);--comm-color-zinc-50:oklch(.985 0 0);--comm-color-zinc-100:oklch(.967 .001 286.375);--comm-color-zinc-200:oklch(.92 .004 286.32);--comm-color-zinc-300:oklch(.871 .006 286.286);--comm-color-zinc-400:oklch(.705 .015 286.067);--comm-color-zinc-500:oklch(.552 .016 285.938);--comm-color-zinc-600:oklch(.442 .017 285.786);--comm-color-zinc-700:oklch(.37 .013 285.805);--comm-color-zinc-800:oklch(.274 .006 286.033);--comm-color-zinc-900:oklch(.21 .006 285.885);--comm-color-zinc-950:oklch(.141 .005 285.823);--comm-color-neutral-50:oklch(.985 0 0);--comm-color-neutral-100:oklch(.97 0 0);--comm-color-neutral-200:oklch(.922 0 0);--comm-color-neutral-300:oklch(.87 0 0);--comm-color-neutral-400:oklch(.708 0 0);--comm-color-neutral-500:oklch(.556 0 0);--comm-color-neutral-600:oklch(.439 0 0);--comm-color-neutral-700:oklch(.371 0 0);--comm-color-neutral-800:oklch(.269 0 0);--comm-color-neutral-900:oklch(.205 0 0);--comm-color-neutral-950:oklch(.145 0 0);--comm-color-stone-50:oklch(.985 .001 106.423);--comm-color-stone-100:oklch(.97 .001 106.424);--comm-color-stone-200:oklch(.923 .003 48.717);--comm-color-stone-300:oklch(.869 .005 56.366);--comm-color-stone-400:oklch(.709 .01 56.259);--comm-color-stone-500:oklch(.553 .013 58.071);--comm-color-stone-600:oklch(.444 .011 73.639);--comm-color-stone-700:oklch(.374 .01 67.558);--comm-color-stone-800:oklch(.268 .007 34.298);--comm-color-stone-900:oklch(.216 .006 56.043);--comm-color-stone-950:oklch(.147 .004 49.25);--comm-color-black:#000;--comm-color-white:#fff;--comm-spacing:.25rem;--comm-breakpoint-sm:40rem;--comm-breakpoint-md:48rem;--comm-breakpoint-lg:64rem;--comm-breakpoint-xl:80rem;--comm-breakpoint-2xl:96rem;--comm-container-3xs:16rem;--comm-container-2xs:18rem;--comm-container-xs:20rem;--comm-container-sm:24rem;--comm-container-md:28rem;--comm-container-lg:32rem;--comm-container-xl:36rem;--comm-container-2xl:42rem;--comm-container-3xl:48rem;--comm-container-4xl:56rem;--comm-container-5xl:64rem;--comm-container-6xl:72rem;--comm-container-7xl:80rem;--comm-text-xs:.75rem;--comm-text-xs--line-height:calc(1/.75);--comm-text-sm:.875rem;--comm-text-sm--line-height:calc(1.25/.875);--comm-text-base:1rem;--comm-text-base--line-height:calc(1.5/1);--comm-text-lg:1.125rem;--comm-text-lg--line-height:calc(1.75/1.125);--comm-text-xl:1.25rem;--comm-text-xl--line-height:calc(1.75/1.25);--comm-text-2xl:1.5rem;--comm-text-2xl--line-height:calc(2/1.5);--comm-text-3xl:1.875rem;--comm-text-3xl--line-height:calc(2.25/1.875);--comm-text-4xl:2.25rem;--comm-text-4xl--line-height:calc(2.5/2.25);--comm-text-5xl:3rem;--comm-text-5xl--line-height:1;--comm-text-6xl:3.75rem;--comm-text-6xl--line-height:1;--comm-text-7xl:4.5rem;--comm-text-7xl--line-height:1;--comm-text-8xl:6rem;--comm-text-8xl--line-height:1;--comm-text-9xl:8rem;--comm-text-9xl--line-height:1;--comm-font-weight-thin:100;--comm-font-weight-extralight:200;--comm-font-weight-light:300;--comm-font-weight-normal:400;--comm-font-weight-medium:500;--comm-font-weight-semibold:600;--comm-font-weight-bold:700;--comm-font-weight-extrabold:800;--comm-font-weight-black:900;--comm-tracking-tighter:-.05em;--comm-tracking-tight:-.025em;--comm-tracking-normal:0em;--comm-tracking-wide:.025em;--comm-tracking-wider:.05em;--comm-tracking-widest:.1em;--comm-leading-tight:1.25;--comm-leading-snug:1.375;--comm-leading-normal:1.5;--comm-leading-relaxed:1.625;--comm-leading-loose:2;--comm-radius-xs:.125rem;--comm-radius-sm:.25rem;--comm-radius-md:.375rem;--comm-radius-lg:.5rem;--comm-radius-xl:.75rem;--comm-radius-2xl:1rem;--comm-radius-3xl:1.5rem;--comm-radius-4xl:2rem;--comm-shadow-2xs:0 1px #0000000d;--comm-shadow-xs:0 1px 2px 0 #0000000d;--comm-shadow-sm:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--comm-shadow-md:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--comm-shadow-lg:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--comm-shadow-xl:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--comm-shadow-2xl:0 25px 50px -12px #00000040;--comm-inset-shadow-2xs:inset 0 1px #0000000d;--comm-inset-shadow-xs:inset 0 1px 1px #0000000d;--comm-inset-shadow-sm:inset 0 2px 4px #0000000d;--comm-drop-shadow-xs:0 1px 1px #0000000d;--comm-drop-shadow-sm:0 1px 2px #00000026;--comm-drop-shadow-md:0 3px 3px #0000001f;--comm-drop-shadow-lg:0 4px 4px #00000026;--comm-drop-shadow-xl:0 9px 7px #0000001a;--comm-drop-shadow-2xl:0 25px 25px #00000026;--comm-ease-in:cubic-bezier(.4,0,1,1);--comm-ease-out:cubic-bezier(0,0,.2,1);--comm-ease-in-out:cubic-bezier(.4,0,.2,1);--comm-animate-spin:spin 1s linear infinite;--comm-animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--comm-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--comm-animate-bounce:bounce 1s infinite;--comm-blur-xs:4px;--comm-blur-sm:8px;--comm-blur-md:12px;--comm-blur-lg:16px;--comm-blur-xl:24px;--comm-blur-2xl:40px;--comm-blur-3xl:64px;--comm-perspective-dramatic:100px;--comm-perspective-near:300px;--comm-perspective-normal:500px;--comm-perspective-midrange:800px;--comm-perspective-distant:1200px;--comm-aspect-video:16/9;--comm-default-transition-duration:.15s;--comm-default-transition-timing-function:cubic-bezier(.4,0,.2,1);--comm-default-font-family:var(--font-sans);--comm-default-font-feature-settings:var(--font-sans--font-feature-settings);--comm-default-font-variation-settings:var(--font-sans--font-variation-settings);--comm-default-mono-font-family:var(--font-mono);--comm-default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--comm-default-mono-font-variation-settings:var(--font-mono--font-variation-settings)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;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}body{line-height:inherit}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;-webkit-text-decoration: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{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.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{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}: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;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{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.comm\:absolute{position:absolute!important}.comm\:relative{position:relative!important}.comm\:sticky{position:sticky!important}.comm\:top-4{top:calc(var(--comm-spacing)*4)!important}.comm\:bottom-full{bottom:100%!important}.comm\:z-10{z-index:10!important}.comm\:mt-0\.5{margin-top:calc(var(--comm-spacing)*.5)!important}.comm\:mt-1{margin-top:calc(var(--comm-spacing)*1)!important}.comm\:mt-2{margin-top:calc(var(--comm-spacing)*2)!important}.comm\:mb-2{margin-bottom:calc(var(--comm-spacing)*2)!important}.comm\:mb-3{margin-bottom:calc(var(--comm-spacing)*3)!important}.comm\:ml-1{margin-left:calc(var(--comm-spacing)*1)!important}.comm\:ml-2{margin-left:calc(var(--comm-spacing)*2)!important}.comm\:flex{display:flex!important}.comm\:hidden{display:none!important}.comm\:inline-block{display:inline-block!important}.comm\:inline-flex{display:inline-flex!important}.comm\:h-3{height:calc(var(--comm-spacing)*3)!important}.comm\:h-4{height:calc(var(--comm-spacing)*4)!important}.comm\:h-8{height:calc(var(--comm-spacing)*8)!important}.comm\:h-10{height:calc(var(--comm-spacing)*10)!important}.comm\:h-full{height:100%!important}.comm\:w-3{width:calc(var(--comm-spacing)*3)!important}.comm\:w-4{width:calc(var(--comm-spacing)*4)!important}.comm\:w-8{width:calc(var(--comm-spacing)*8)!important}.comm\:w-10{width:calc(var(--comm-spacing)*10)!important}.comm\:w-48{width:calc(var(--comm-spacing)*48)!important}.comm\:w-full{width:100%!important}.comm\:w-max{width:max-content!important}.comm\:max-w-xs{max-width:var(--comm-container-xs)!important}.comm\:min-w-full{min-width:100%!important}.comm\:flex-1{flex:1!important}.comm\:flex-shrink-0{flex-shrink:0!important}.comm\:flex-col{flex-direction:column!important}.comm\:flex-wrap{flex-wrap:wrap!important}.comm\:items-center{align-items:center!important}.comm\:items-start{align-items:flex-start!important}.comm\:justify-between{justify-content:space-between!important}.comm\:justify-center{justify-content:center!important}.comm\:justify-end{justify-content:flex-end!important}.comm\:gap-1{gap:calc(var(--comm-spacing)*1)!important}.comm\:gap-2{gap:calc(var(--comm-spacing)*2)!important}.comm\:gap-4{gap:calc(var(--comm-spacing)*4)!important}.comm\:gap-x-1{column-gap:calc(var(--comm-spacing)*1)!important}.comm\:gap-x-2{column-gap:calc(var(--comm-spacing)*2)!important}.comm\:gap-x-4{column-gap:calc(var(--comm-spacing)*4)!important}:where(.comm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*1)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*1)*calc(1 - var(--tw-space-y-reverse)))!important}:where(.comm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*2)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*2)*calc(1 - var(--tw-space-y-reverse)))!important}:where(.comm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*6)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*6)*calc(1 - var(--tw-space-y-reverse)))!important}.comm\:gap-y-2{row-gap:calc(var(--comm-spacing)*2)!important}.comm\:truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.comm\:rounded-full{border-radius:3.40282e38px!important}.comm\:rounded-lg{border-radius:var(--comm-radius-lg)!important}.comm\:rounded-md{border-radius:var(--comm-radius-md)!important}.comm\:border{border-style:var(--tw-border-style)!important;border-width:1px!important}.comm\:border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.comm\:border-dashed{--tw-border-style:dashed!important;border-style:dashed!important}.comm\:border-gray-200{border-color:var(--comm-color-gray-200)!important}.comm\:border-gray-300{border-color:var(--comm-color-gray-300)!important}.comm\:bg-blue-100{background-color:var(--comm-color-blue-100)!important}.comm\:bg-gray-50{background-color:var(--comm-color-gray-50)!important}.comm\:bg-gray-100{background-color:var(--comm-color-gray-100)!important}.comm\:bg-gray-300{background-color:var(--comm-color-gray-300)!important}.comm\:bg-white{background-color:var(--comm-color-white)!important}.comm\:object-cover{object-fit:cover!important}.comm\:object-center{object-position:center!important}.comm\:p-1{padding:calc(var(--comm-spacing)*1)!important}.comm\:p-2{padding:calc(var(--comm-spacing)*2)!important}.comm\:p-4{padding:calc(var(--comm-spacing)*4)!important}.comm\:p-6{padding:calc(var(--comm-spacing)*6)!important}.comm\:px-1\.5{padding-inline:calc(var(--comm-spacing)*1.5)!important}.comm\:px-2{padding-inline:calc(var(--comm-spacing)*2)!important}.comm\:py-0\.5{padding-block:calc(var(--comm-spacing)*.5)!important}.comm\:py-4{padding-block:calc(var(--comm-spacing)*4)!important}.comm\:pt-2{padding-top:calc(var(--comm-spacing)*2)!important}.comm\:pt-3{padding-top:calc(var(--comm-spacing)*3)!important}.comm\:pl-2{padding-left:calc(var(--comm-spacing)*2)!important}.comm\:pl-6{padding-left:calc(var(--comm-spacing)*6)!important}.comm\:text-center{text-align:center!important}.comm\:text-sm{font-size:var(--comm-text-sm)!important;line-height:var(--tw-leading,var(--comm-text-sm--line-height))!important}.comm\:text-xs{font-size:var(--comm-text-xs)!important;line-height:var(--tw-leading,var(--comm-text-xs--line-height))!important}.comm\:font-bold{--tw-font-weight:var(--comm-font-weight-bold)!important;font-weight:var(--comm-font-weight-bold)!important}.comm\:font-medium{--tw-font-weight:var(--comm-font-weight-medium)!important;font-weight:var(--comm-font-weight-medium)!important}.comm\:whitespace-nowrap{white-space:nowrap!important}.comm\:text-gray-300{color:var(--comm-color-gray-300)!important}.comm\:text-gray-400{color:var(--comm-color-gray-400)!important}.comm\:text-gray-500{color:var(--comm-color-gray-500)!important}.comm\:text-gray-600{color:var(--comm-color-gray-600)!important}.comm\:text-gray-700{color:var(--comm-color-gray-700)!important}.comm\:text-gray-800{color:var(--comm-color-gray-800)!important}.comm\:text-gray-900{color:var(--comm-color-gray-900)!important}.comm\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\: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!important;transition-timing-function:var(--tw-ease,var(--comm-default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--comm-default-transition-duration))!important}@media (hover:hover){.comm\:hover\:bg-gray-100:hover{background-color:var(--comm-color-gray-100)!important}.comm\:hover\:bg-gray-200:hover{background-color:var(--comm-color-gray-200)!important}}.comm\:focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\:focus\:ring-offset-2:focus{--tw-ring-offset-width:2px!important;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)!important}.comm\:focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.comm\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.comm\:disabled\:opacity-50:disabled{opacity:.5!important}.comm\:dark\:border-gray-600:where(.dark,.dark *){border-color:var(--comm-color-gray-600)!important}.comm\:dark\:border-gray-700:where(.dark,.dark *){border-color:var(--comm-color-gray-700)!important}.comm\:dark\:bg-gray-600:where(.dark,.dark *){background-color:var(--comm-color-gray-600)!important}.comm\:dark\:bg-gray-800:where(.dark,.dark *){background-color:var(--comm-color-gray-800)!important}.comm\:dark\:bg-gray-900:where(.dark,.dark *){background-color:var(--comm-color-gray-900)!important}.comm\:dark\:text-gray-100:where(.dark,.dark *){color:var(--comm-color-gray-100)!important}.comm\:dark\:text-gray-200:where(.dark,.dark *){color:var(--comm-color-gray-200)!important}.comm\:dark\:text-gray-300:where(.dark,.dark *){color:var(--comm-color-gray-300)!important}.comm\:dark\:text-gray-400:where(.dark,.dark *){color:var(--comm-color-gray-400)!important}.comm\:dark\:text-gray-500:where(.dark,.dark *){color:var(--comm-color-gray-500)!important}@media (hover:hover){.comm\:dark\:hover\:bg-gray-600:where(.dark,.dark *):hover{background-color:var(--comm-color-gray-600)!important}.comm\:dark\:hover\:bg-gray-700:where(.dark,.dark *):hover{background-color:var(--comm-color-gray-700)!important}}}[x-cloak]{display:none}.tiptap{font-size:var(--comm-text-sm)!important;line-height:var(--tw-leading,var(--comm-text-sm--line-height))!important;--tw-leading:var(--comm-leading-normal)!important;line-height:var(--comm-leading-normal)!important}.tiptap p.is-editor-empty:before{color:#adb5bd;content:attr(data-placeholder);float:inline-start;pointer-events:none;height:0}.tiptap .mention{background-color:var(--comm-color-gray-200)!important;padding-inline:calc(var(--comm-spacing)*1)!important;padding-block:calc(var(--comm-spacing)*.5)!important;--tw-font-weight:var(--comm-font-weight-bold)!important;font-weight:var(--comm-font-weight-bold)!important;color:var(--comm-color-blue-500)!important;border-radius:.25rem!important;display:inline-block!important}.mention-suggestion{z-index:1000;background:#fff;border:1px solid #ddd;border-radius:5px;max-height:150px;padding:5px;overflow-y:auto}.mention-item{cursor:pointer;padding:5px 10px}.mention-item:hover{background-color:var(--comm-color-gray-100)!important}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false} \ No newline at end of file +@layer theme{:root,:host{--comm-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--comm-font-serif:ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--comm-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--comm-color-red-50:oklch(.971 .013 17.38);--comm-color-red-100:oklch(.936 .032 17.717);--comm-color-red-200:oklch(.885 .062 18.334);--comm-color-red-300:oklch(.808 .114 19.571);--comm-color-red-400:oklch(.704 .191 22.216);--comm-color-red-500:oklch(.637 .237 25.331);--comm-color-red-600:oklch(.577 .245 27.325);--comm-color-red-700:oklch(.505 .213 27.518);--comm-color-red-800:oklch(.444 .177 26.899);--comm-color-red-900:oklch(.396 .141 25.723);--comm-color-red-950:oklch(.258 .092 26.042);--comm-color-orange-50:oklch(.98 .016 73.684);--comm-color-orange-100:oklch(.954 .038 75.164);--comm-color-orange-200:oklch(.901 .076 70.697);--comm-color-orange-300:oklch(.837 .128 66.29);--comm-color-orange-400:oklch(.75 .183 55.934);--comm-color-orange-500:oklch(.705 .213 47.604);--comm-color-orange-600:oklch(.646 .222 41.116);--comm-color-orange-700:oklch(.553 .195 38.402);--comm-color-orange-800:oklch(.47 .157 37.304);--comm-color-orange-900:oklch(.408 .123 38.172);--comm-color-orange-950:oklch(.266 .079 36.259);--comm-color-amber-50:oklch(.987 .022 95.277);--comm-color-amber-100:oklch(.962 .059 95.617);--comm-color-amber-200:oklch(.924 .12 95.746);--comm-color-amber-300:oklch(.879 .169 91.605);--comm-color-amber-400:oklch(.828 .189 84.429);--comm-color-amber-500:oklch(.769 .188 70.08);--comm-color-amber-600:oklch(.666 .179 58.318);--comm-color-amber-700:oklch(.555 .163 48.998);--comm-color-amber-800:oklch(.473 .137 46.201);--comm-color-amber-900:oklch(.414 .112 45.904);--comm-color-amber-950:oklch(.279 .077 45.635);--comm-color-yellow-50:oklch(.987 .026 102.212);--comm-color-yellow-100:oklch(.973 .071 103.193);--comm-color-yellow-200:oklch(.945 .129 101.54);--comm-color-yellow-300:oklch(.905 .182 98.111);--comm-color-yellow-400:oklch(.852 .199 91.936);--comm-color-yellow-500:oklch(.795 .184 86.047);--comm-color-yellow-600:oklch(.681 .162 75.834);--comm-color-yellow-700:oklch(.554 .135 66.442);--comm-color-yellow-800:oklch(.476 .114 61.907);--comm-color-yellow-900:oklch(.421 .095 57.708);--comm-color-yellow-950:oklch(.286 .066 53.813);--comm-color-lime-50:oklch(.986 .031 120.757);--comm-color-lime-100:oklch(.967 .067 122.328);--comm-color-lime-200:oklch(.938 .127 124.321);--comm-color-lime-300:oklch(.897 .196 126.665);--comm-color-lime-400:oklch(.841 .238 128.85);--comm-color-lime-500:oklch(.768 .233 130.85);--comm-color-lime-600:oklch(.648 .2 131.684);--comm-color-lime-700:oklch(.532 .157 131.589);--comm-color-lime-800:oklch(.453 .124 130.933);--comm-color-lime-900:oklch(.405 .101 131.063);--comm-color-lime-950:oklch(.274 .072 132.109);--comm-color-green-50:oklch(.982 .018 155.826);--comm-color-green-100:oklch(.962 .044 156.743);--comm-color-green-200:oklch(.925 .084 155.995);--comm-color-green-300:oklch(.871 .15 154.449);--comm-color-green-400:oklch(.792 .209 151.711);--comm-color-green-500:oklch(.723 .219 149.579);--comm-color-green-600:oklch(.627 .194 149.214);--comm-color-green-700:oklch(.527 .154 150.069);--comm-color-green-800:oklch(.448 .119 151.328);--comm-color-green-900:oklch(.393 .095 152.535);--comm-color-green-950:oklch(.266 .065 152.934);--comm-color-emerald-50:oklch(.979 .021 166.113);--comm-color-emerald-100:oklch(.95 .052 163.051);--comm-color-emerald-200:oklch(.905 .093 164.15);--comm-color-emerald-300:oklch(.845 .143 164.978);--comm-color-emerald-400:oklch(.765 .177 163.223);--comm-color-emerald-500:oklch(.696 .17 162.48);--comm-color-emerald-600:oklch(.596 .145 163.225);--comm-color-emerald-700:oklch(.508 .118 165.612);--comm-color-emerald-800:oklch(.432 .095 166.913);--comm-color-emerald-900:oklch(.378 .077 168.94);--comm-color-emerald-950:oklch(.262 .051 172.552);--comm-color-teal-50:oklch(.984 .014 180.72);--comm-color-teal-100:oklch(.953 .051 180.801);--comm-color-teal-200:oklch(.91 .096 180.426);--comm-color-teal-300:oklch(.855 .138 181.071);--comm-color-teal-400:oklch(.777 .152 181.912);--comm-color-teal-500:oklch(.704 .14 182.503);--comm-color-teal-600:oklch(.6 .118 184.704);--comm-color-teal-700:oklch(.511 .096 186.391);--comm-color-teal-800:oklch(.437 .078 188.216);--comm-color-teal-900:oklch(.386 .063 188.416);--comm-color-teal-950:oklch(.277 .046 192.524);--comm-color-cyan-50:oklch(.984 .019 200.873);--comm-color-cyan-100:oklch(.956 .045 203.388);--comm-color-cyan-200:oklch(.917 .08 205.041);--comm-color-cyan-300:oklch(.865 .127 207.078);--comm-color-cyan-400:oklch(.789 .154 211.53);--comm-color-cyan-500:oklch(.715 .143 215.221);--comm-color-cyan-600:oklch(.609 .126 221.723);--comm-color-cyan-700:oklch(.52 .105 223.128);--comm-color-cyan-800:oklch(.45 .085 224.283);--comm-color-cyan-900:oklch(.398 .07 227.392);--comm-color-cyan-950:oklch(.302 .056 229.695);--comm-color-sky-50:oklch(.977 .013 236.62);--comm-color-sky-100:oklch(.951 .026 236.824);--comm-color-sky-200:oklch(.901 .058 230.902);--comm-color-sky-300:oklch(.828 .111 230.318);--comm-color-sky-400:oklch(.746 .16 232.661);--comm-color-sky-500:oklch(.685 .169 237.323);--comm-color-sky-600:oklch(.588 .158 241.966);--comm-color-sky-700:oklch(.5 .134 242.749);--comm-color-sky-800:oklch(.443 .11 240.79);--comm-color-sky-900:oklch(.391 .09 240.876);--comm-color-sky-950:oklch(.293 .066 243.157);--comm-color-blue-50:oklch(.97 .014 254.604);--comm-color-blue-100:oklch(.932 .032 255.585);--comm-color-blue-200:oklch(.882 .059 254.128);--comm-color-blue-300:oklch(.809 .105 251.813);--comm-color-blue-400:oklch(.707 .165 254.624);--comm-color-blue-500:oklch(.623 .214 259.815);--comm-color-blue-600:oklch(.546 .245 262.881);--comm-color-blue-700:oklch(.488 .243 264.376);--comm-color-blue-800:oklch(.424 .199 265.638);--comm-color-blue-900:oklch(.379 .146 265.522);--comm-color-blue-950:oklch(.282 .091 267.935);--comm-color-indigo-50:oklch(.962 .018 272.314);--comm-color-indigo-100:oklch(.93 .034 272.788);--comm-color-indigo-200:oklch(.87 .065 274.039);--comm-color-indigo-300:oklch(.785 .115 274.713);--comm-color-indigo-400:oklch(.673 .182 276.935);--comm-color-indigo-500:oklch(.585 .233 277.117);--comm-color-indigo-600:oklch(.511 .262 276.966);--comm-color-indigo-700:oklch(.457 .24 277.023);--comm-color-indigo-800:oklch(.398 .195 277.366);--comm-color-indigo-900:oklch(.359 .144 278.697);--comm-color-indigo-950:oklch(.257 .09 281.288);--comm-color-violet-50:oklch(.969 .016 293.756);--comm-color-violet-100:oklch(.943 .029 294.588);--comm-color-violet-200:oklch(.894 .057 293.283);--comm-color-violet-300:oklch(.811 .111 293.571);--comm-color-violet-400:oklch(.702 .183 293.541);--comm-color-violet-500:oklch(.606 .25 292.717);--comm-color-violet-600:oklch(.541 .281 293.009);--comm-color-violet-700:oklch(.491 .27 292.581);--comm-color-violet-800:oklch(.432 .232 292.759);--comm-color-violet-900:oklch(.38 .189 293.745);--comm-color-violet-950:oklch(.283 .141 291.089);--comm-color-purple-50:oklch(.977 .014 308.299);--comm-color-purple-100:oklch(.946 .033 307.174);--comm-color-purple-200:oklch(.902 .063 306.703);--comm-color-purple-300:oklch(.827 .119 306.383);--comm-color-purple-400:oklch(.714 .203 305.504);--comm-color-purple-500:oklch(.627 .265 303.9);--comm-color-purple-600:oklch(.558 .288 302.321);--comm-color-purple-700:oklch(.496 .265 301.924);--comm-color-purple-800:oklch(.438 .218 303.724);--comm-color-purple-900:oklch(.381 .176 304.987);--comm-color-purple-950:oklch(.291 .149 302.717);--comm-color-fuchsia-50:oklch(.977 .017 320.058);--comm-color-fuchsia-100:oklch(.952 .037 318.852);--comm-color-fuchsia-200:oklch(.903 .076 319.62);--comm-color-fuchsia-300:oklch(.833 .145 321.434);--comm-color-fuchsia-400:oklch(.74 .238 322.16);--comm-color-fuchsia-500:oklch(.667 .295 322.15);--comm-color-fuchsia-600:oklch(.591 .293 322.896);--comm-color-fuchsia-700:oklch(.518 .253 323.949);--comm-color-fuchsia-800:oklch(.452 .211 324.591);--comm-color-fuchsia-900:oklch(.401 .17 325.612);--comm-color-fuchsia-950:oklch(.293 .136 325.661);--comm-color-pink-50:oklch(.971 .014 343.198);--comm-color-pink-100:oklch(.948 .028 342.258);--comm-color-pink-200:oklch(.899 .061 343.231);--comm-color-pink-300:oklch(.823 .12 346.018);--comm-color-pink-400:oklch(.718 .202 349.761);--comm-color-pink-500:oklch(.656 .241 354.308);--comm-color-pink-600:oklch(.592 .249 .584);--comm-color-pink-700:oklch(.525 .223 3.958);--comm-color-pink-800:oklch(.459 .187 3.815);--comm-color-pink-900:oklch(.408 .153 2.432);--comm-color-pink-950:oklch(.284 .109 3.907);--comm-color-rose-50:oklch(.969 .015 12.422);--comm-color-rose-100:oklch(.941 .03 12.58);--comm-color-rose-200:oklch(.892 .058 10.001);--comm-color-rose-300:oklch(.81 .117 11.638);--comm-color-rose-400:oklch(.712 .194 13.428);--comm-color-rose-500:oklch(.645 .246 16.439);--comm-color-rose-600:oklch(.586 .253 17.585);--comm-color-rose-700:oklch(.514 .222 16.935);--comm-color-rose-800:oklch(.455 .188 13.697);--comm-color-rose-900:oklch(.41 .159 10.272);--comm-color-rose-950:oklch(.271 .105 12.094);--comm-color-slate-50:oklch(.984 .003 247.858);--comm-color-slate-100:oklch(.968 .007 247.896);--comm-color-slate-200:oklch(.929 .013 255.508);--comm-color-slate-300:oklch(.869 .022 252.894);--comm-color-slate-400:oklch(.704 .04 256.788);--comm-color-slate-500:oklch(.554 .046 257.417);--comm-color-slate-600:oklch(.446 .043 257.281);--comm-color-slate-700:oklch(.372 .044 257.287);--comm-color-slate-800:oklch(.279 .041 260.031);--comm-color-slate-900:oklch(.208 .042 265.755);--comm-color-slate-950:oklch(.129 .042 264.695);--comm-color-gray-50:oklch(.985 .002 247.839);--comm-color-gray-100:oklch(.967 .003 264.542);--comm-color-gray-200:oklch(.928 .006 264.531);--comm-color-gray-300:oklch(.872 .01 258.338);--comm-color-gray-400:oklch(.707 .022 261.325);--comm-color-gray-500:oklch(.551 .027 264.364);--comm-color-gray-600:oklch(.446 .03 256.802);--comm-color-gray-700:oklch(.373 .034 259.733);--comm-color-gray-800:oklch(.278 .033 256.848);--comm-color-gray-900:oklch(.21 .034 264.665);--comm-color-gray-950:oklch(.13 .028 261.692);--comm-color-zinc-50:oklch(.985 0 0);--comm-color-zinc-100:oklch(.967 .001 286.375);--comm-color-zinc-200:oklch(.92 .004 286.32);--comm-color-zinc-300:oklch(.871 .006 286.286);--comm-color-zinc-400:oklch(.705 .015 286.067);--comm-color-zinc-500:oklch(.552 .016 285.938);--comm-color-zinc-600:oklch(.442 .017 285.786);--comm-color-zinc-700:oklch(.37 .013 285.805);--comm-color-zinc-800:oklch(.274 .006 286.033);--comm-color-zinc-900:oklch(.21 .006 285.885);--comm-color-zinc-950:oklch(.141 .005 285.823);--comm-color-neutral-50:oklch(.985 0 0);--comm-color-neutral-100:oklch(.97 0 0);--comm-color-neutral-200:oklch(.922 0 0);--comm-color-neutral-300:oklch(.87 0 0);--comm-color-neutral-400:oklch(.708 0 0);--comm-color-neutral-500:oklch(.556 0 0);--comm-color-neutral-600:oklch(.439 0 0);--comm-color-neutral-700:oklch(.371 0 0);--comm-color-neutral-800:oklch(.269 0 0);--comm-color-neutral-900:oklch(.205 0 0);--comm-color-neutral-950:oklch(.145 0 0);--comm-color-stone-50:oklch(.985 .001 106.423);--comm-color-stone-100:oklch(.97 .001 106.424);--comm-color-stone-200:oklch(.923 .003 48.717);--comm-color-stone-300:oklch(.869 .005 56.366);--comm-color-stone-400:oklch(.709 .01 56.259);--comm-color-stone-500:oklch(.553 .013 58.071);--comm-color-stone-600:oklch(.444 .011 73.639);--comm-color-stone-700:oklch(.374 .01 67.558);--comm-color-stone-800:oklch(.268 .007 34.298);--comm-color-stone-900:oklch(.216 .006 56.043);--comm-color-stone-950:oklch(.147 .004 49.25);--comm-color-black:#000;--comm-color-white:#fff;--comm-spacing:.25rem;--comm-breakpoint-sm:40rem;--comm-breakpoint-md:48rem;--comm-breakpoint-lg:64rem;--comm-breakpoint-xl:80rem;--comm-breakpoint-2xl:96rem;--comm-container-3xs:16rem;--comm-container-2xs:18rem;--comm-container-xs:20rem;--comm-container-sm:24rem;--comm-container-md:28rem;--comm-container-lg:32rem;--comm-container-xl:36rem;--comm-container-2xl:42rem;--comm-container-3xl:48rem;--comm-container-4xl:56rem;--comm-container-5xl:64rem;--comm-container-6xl:72rem;--comm-container-7xl:80rem;--comm-text-xs:.75rem;--comm-text-xs--line-height:calc(1/.75);--comm-text-sm:.875rem;--comm-text-sm--line-height:calc(1.25/.875);--comm-text-base:1rem;--comm-text-base--line-height:calc(1.5/1);--comm-text-lg:1.125rem;--comm-text-lg--line-height:calc(1.75/1.125);--comm-text-xl:1.25rem;--comm-text-xl--line-height:calc(1.75/1.25);--comm-text-2xl:1.5rem;--comm-text-2xl--line-height:calc(2/1.5);--comm-text-3xl:1.875rem;--comm-text-3xl--line-height:calc(2.25/1.875);--comm-text-4xl:2.25rem;--comm-text-4xl--line-height:calc(2.5/2.25);--comm-text-5xl:3rem;--comm-text-5xl--line-height:1;--comm-text-6xl:3.75rem;--comm-text-6xl--line-height:1;--comm-text-7xl:4.5rem;--comm-text-7xl--line-height:1;--comm-text-8xl:6rem;--comm-text-8xl--line-height:1;--comm-text-9xl:8rem;--comm-text-9xl--line-height:1;--comm-font-weight-thin:100;--comm-font-weight-extralight:200;--comm-font-weight-light:300;--comm-font-weight-normal:400;--comm-font-weight-medium:500;--comm-font-weight-semibold:600;--comm-font-weight-bold:700;--comm-font-weight-extrabold:800;--comm-font-weight-black:900;--comm-tracking-tighter:-.05em;--comm-tracking-tight:-.025em;--comm-tracking-normal:0em;--comm-tracking-wide:.025em;--comm-tracking-wider:.05em;--comm-tracking-widest:.1em;--comm-leading-tight:1.25;--comm-leading-snug:1.375;--comm-leading-normal:1.5;--comm-leading-relaxed:1.625;--comm-leading-loose:2;--comm-radius-xs:.125rem;--comm-radius-sm:.25rem;--comm-radius-md:.375rem;--comm-radius-lg:.5rem;--comm-radius-xl:.75rem;--comm-radius-2xl:1rem;--comm-radius-3xl:1.5rem;--comm-radius-4xl:2rem;--comm-shadow-2xs:0 1px #0000000d;--comm-shadow-xs:0 1px 2px 0 #0000000d;--comm-shadow-sm:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--comm-shadow-md:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--comm-shadow-lg:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--comm-shadow-xl:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--comm-shadow-2xl:0 25px 50px -12px #00000040;--comm-inset-shadow-2xs:inset 0 1px #0000000d;--comm-inset-shadow-xs:inset 0 1px 1px #0000000d;--comm-inset-shadow-sm:inset 0 2px 4px #0000000d;--comm-drop-shadow-xs:0 1px 1px #0000000d;--comm-drop-shadow-sm:0 1px 2px #00000026;--comm-drop-shadow-md:0 3px 3px #0000001f;--comm-drop-shadow-lg:0 4px 4px #00000026;--comm-drop-shadow-xl:0 9px 7px #0000001a;--comm-drop-shadow-2xl:0 25px 25px #00000026;--comm-ease-in:cubic-bezier(.4,0,1,1);--comm-ease-out:cubic-bezier(0,0,.2,1);--comm-ease-in-out:cubic-bezier(.4,0,.2,1);--comm-animate-spin:spin 1s linear infinite;--comm-animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--comm-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--comm-animate-bounce:bounce 1s infinite;--comm-blur-xs:4px;--comm-blur-sm:8px;--comm-blur-md:12px;--comm-blur-lg:16px;--comm-blur-xl:24px;--comm-blur-2xl:40px;--comm-blur-3xl:64px;--comm-perspective-dramatic:100px;--comm-perspective-near:300px;--comm-perspective-normal:500px;--comm-perspective-midrange:800px;--comm-perspective-distant:1200px;--comm-aspect-video:16/9;--comm-default-transition-duration:.15s;--comm-default-transition-timing-function:cubic-bezier(.4,0,.2,1);--comm-default-font-family:var(--font-sans);--comm-default-font-feature-settings:var(--font-sans--font-feature-settings);--comm-default-font-variation-settings:var(--font-sans--font-variation-settings);--comm-default-mono-font-family:var(--font-mono);--comm-default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--comm-default-mono-font-variation-settings:var(--font-mono--font-variation-settings)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;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}body{line-height:inherit}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;-webkit-text-decoration: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{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.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{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}: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;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{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.comm\:absolute{position:absolute!important}.comm\:relative{position:relative!important}.comm\:sticky{position:sticky!important}.comm\:top-4{top:calc(var(--comm-spacing)*4)!important}.comm\:bottom-full{bottom:100%!important}.comm\:z-10{z-index:10!important}.comm\:mt-0\.5{margin-top:calc(var(--comm-spacing)*.5)!important}.comm\:mt-1{margin-top:calc(var(--comm-spacing)*1)!important}.comm\:mt-2{margin-top:calc(var(--comm-spacing)*2)!important}.comm\:mb-2{margin-bottom:calc(var(--comm-spacing)*2)!important}.comm\:mb-3{margin-bottom:calc(var(--comm-spacing)*3)!important}.comm\:ml-1{margin-left:calc(var(--comm-spacing)*1)!important}.comm\:ml-2{margin-left:calc(var(--comm-spacing)*2)!important}.comm\:flex{display:flex!important}.comm\:hidden{display:none!important}.comm\:inline-block{display:inline-block!important}.comm\:inline-flex{display:inline-flex!important}.comm\:h-3{height:calc(var(--comm-spacing)*3)!important}.comm\:h-4{height:calc(var(--comm-spacing)*4)!important}.comm\:h-5{height:calc(var(--comm-spacing)*5)!important}.comm\:h-8{height:calc(var(--comm-spacing)*8)!important}.comm\:h-10{height:calc(var(--comm-spacing)*10)!important}.comm\:h-full{height:100%!important}.comm\:w-3{width:calc(var(--comm-spacing)*3)!important}.comm\:w-4{width:calc(var(--comm-spacing)*4)!important}.comm\:w-5{width:calc(var(--comm-spacing)*5)!important}.comm\:w-8{width:calc(var(--comm-spacing)*8)!important}.comm\:w-10{width:calc(var(--comm-spacing)*10)!important}.comm\:w-48{width:calc(var(--comm-spacing)*48)!important}.comm\:w-full{width:100%!important}.comm\:w-max{width:max-content!important}.comm\:max-w-xs{max-width:var(--comm-container-xs)!important}.comm\:min-w-full{min-width:100%!important}.comm\:flex-1{flex:1!important}.comm\:flex-shrink-0{flex-shrink:0!important}.comm\:flex-col{flex-direction:column!important}.comm\:flex-wrap{flex-wrap:wrap!important}.comm\:items-center{align-items:center!important}.comm\:items-start{align-items:flex-start!important}.comm\:justify-between{justify-content:space-between!important}.comm\:justify-center{justify-content:center!important}.comm\:justify-end{justify-content:flex-end!important}.comm\:gap-0\.5{gap:calc(var(--comm-spacing)*.5)!important}.comm\:gap-1{gap:calc(var(--comm-spacing)*1)!important}.comm\:gap-2{gap:calc(var(--comm-spacing)*2)!important}.comm\:gap-4{gap:calc(var(--comm-spacing)*4)!important}.comm\:gap-x-1{column-gap:calc(var(--comm-spacing)*1)!important}.comm\:gap-x-2{column-gap:calc(var(--comm-spacing)*2)!important}.comm\:gap-x-4{column-gap:calc(var(--comm-spacing)*4)!important}:where(.comm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*1)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*1)*calc(1 - var(--tw-space-y-reverse)))!important}:where(.comm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*2)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*2)*calc(1 - var(--tw-space-y-reverse)))!important}:where(.comm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0!important;margin-block-start:calc(calc(var(--comm-spacing)*6)*var(--tw-space-y-reverse))!important;margin-block-end:calc(calc(var(--comm-spacing)*6)*calc(1 - var(--tw-space-y-reverse)))!important}.comm\:gap-y-2{row-gap:calc(var(--comm-spacing)*2)!important}.comm\:truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.comm\:rounded-full{border-radius:3.40282e38px!important}.comm\:rounded-lg{border-radius:var(--comm-radius-lg)!important}.comm\:rounded-md{border-radius:var(--comm-radius-md)!important}.comm\:border{border-style:var(--tw-border-style)!important;border-width:1px!important}.comm\:border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.comm\:border-dashed{--tw-border-style:dashed!important;border-style:dashed!important}.comm\:border-gray-200{border-color:var(--comm-color-gray-200)!important}.comm\:border-gray-300{border-color:var(--comm-color-gray-300)!important}.comm\:bg-blue-100{background-color:var(--comm-color-blue-100)!important}.comm\:bg-gray-50{background-color:var(--comm-color-gray-50)!important}.comm\:bg-gray-100{background-color:var(--comm-color-gray-100)!important}.comm\:bg-gray-300{background-color:var(--comm-color-gray-300)!important}.comm\:bg-white{background-color:var(--comm-color-white)!important}.comm\:object-cover{object-fit:cover!important}.comm\:object-center{object-position:center!important}.comm\:p-1{padding:calc(var(--comm-spacing)*1)!important}.comm\:p-2{padding:calc(var(--comm-spacing)*2)!important}.comm\:p-4{padding:calc(var(--comm-spacing)*4)!important}.comm\:p-6{padding:calc(var(--comm-spacing)*6)!important}.comm\:px-1\.5{padding-inline:calc(var(--comm-spacing)*1.5)!important}.comm\:px-2{padding-inline:calc(var(--comm-spacing)*2)!important}.comm\:py-0\.5{padding-block:calc(var(--comm-spacing)*.5)!important}.comm\:py-4{padding-block:calc(var(--comm-spacing)*4)!important}.comm\:pt-2{padding-top:calc(var(--comm-spacing)*2)!important}.comm\:pt-3{padding-top:calc(var(--comm-spacing)*3)!important}.comm\:pl-2{padding-left:calc(var(--comm-spacing)*2)!important}.comm\:pl-6{padding-left:calc(var(--comm-spacing)*6)!important}.comm\:text-center{text-align:center!important}.comm\:text-sm{font-size:var(--comm-text-sm)!important;line-height:var(--tw-leading,var(--comm-text-sm--line-height))!important}.comm\:text-xs{font-size:var(--comm-text-xs)!important;line-height:var(--tw-leading,var(--comm-text-xs--line-height))!important}.comm\:font-bold{--tw-font-weight:var(--comm-font-weight-bold)!important;font-weight:var(--comm-font-weight-bold)!important}.comm\:font-medium{--tw-font-weight:var(--comm-font-weight-medium)!important;font-weight:var(--comm-font-weight-medium)!important}.comm\:whitespace-nowrap{white-space:nowrap!important}.comm\:text-amber-400{color:var(--comm-color-amber-400)!important}.comm\:text-gray-300{color:var(--comm-color-gray-300)!important}.comm\:text-gray-400{color:var(--comm-color-gray-400)!important}.comm\:text-gray-500{color:var(--comm-color-gray-500)!important}.comm\:text-gray-600{color:var(--comm-color-gray-600)!important}.comm\:text-gray-700{color:var(--comm-color-gray-700)!important}.comm\:text-gray-800{color:var(--comm-color-gray-800)!important}.comm\:text-gray-900{color:var(--comm-color-gray-900)!important}.comm\:text-red-600{color:var(--comm-color-red-600)!important}.comm\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\: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!important;transition-timing-function:var(--tw-ease,var(--comm-default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--comm-default-transition-duration))!important}@media (hover:hover){.comm\:hover\:bg-gray-100:hover{background-color:var(--comm-color-gray-100)!important}.comm\:hover\:bg-gray-200:hover{background-color:var(--comm-color-gray-200)!important}}.comm\:focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.comm\:focus\:ring-offset-2:focus{--tw-ring-offset-width:2px!important;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)!important}.comm\:focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.comm\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.comm\:disabled\:opacity-50:disabled{opacity:.5!important}.comm\:dark\:border-gray-600:where(.dark,.dark *){border-color:var(--comm-color-gray-600)!important}.comm\:dark\:border-gray-700:where(.dark,.dark *){border-color:var(--comm-color-gray-700)!important}.comm\:dark\:bg-gray-600:where(.dark,.dark *){background-color:var(--comm-color-gray-600)!important}.comm\:dark\:bg-gray-800:where(.dark,.dark *){background-color:var(--comm-color-gray-800)!important}.comm\:dark\:bg-gray-900:where(.dark,.dark *){background-color:var(--comm-color-gray-900)!important}.comm\:dark\:text-gray-100:where(.dark,.dark *){color:var(--comm-color-gray-100)!important}.comm\:dark\:text-gray-200:where(.dark,.dark *){color:var(--comm-color-gray-200)!important}.comm\:dark\:text-gray-300:where(.dark,.dark *){color:var(--comm-color-gray-300)!important}.comm\:dark\:text-gray-400:where(.dark,.dark *){color:var(--comm-color-gray-400)!important}.comm\:dark\:text-gray-500:where(.dark,.dark *){color:var(--comm-color-gray-500)!important}.comm\:dark\:text-gray-600:where(.dark,.dark *){color:var(--comm-color-gray-600)!important}@media (hover:hover){.comm\:dark\:hover\:bg-gray-600:where(.dark,.dark *):hover{background-color:var(--comm-color-gray-600)!important}.comm\:dark\:hover\:bg-gray-700:where(.dark,.dark *):hover{background-color:var(--comm-color-gray-700)!important}}}[x-cloak]{display:none}.tiptap{font-size:var(--comm-text-sm)!important;line-height:var(--tw-leading,var(--comm-text-sm--line-height))!important;--tw-leading:var(--comm-leading-normal)!important;line-height:var(--comm-leading-normal)!important}.tiptap p.is-editor-empty:before{color:#adb5bd;content:attr(data-placeholder);float:inline-start;pointer-events:none;height:0}.tiptap .mention{background-color:var(--comm-color-gray-200)!important;padding-inline:calc(var(--comm-spacing)*1)!important;padding-block:calc(var(--comm-spacing)*.5)!important;--tw-font-weight:var(--comm-font-weight-bold)!important;font-weight:var(--comm-font-weight-bold)!important;color:var(--comm-color-blue-500)!important;border-radius:.25rem!important;display:inline-block!important}.mention-suggestion{z-index:1000;background:#fff;border:1px solid #ddd;border-radius:5px;max-height:150px;padding:5px;overflow-y:auto}.mention-item{cursor:pointer;padding:5px 10px}.mention-item:hover{background-color:var(--comm-color-gray-100)!important}.commentions-rating-star{cursor:pointer!important;color:var(--comm-color-gray-300)!important;justify-content:center!important;align-items:center!important;display:flex!important}.commentions-rating-star:where(.dark,.dark *){color:var(--comm-color-gray-600)!important}.commentions-rating-star:hover,.commentions-rating-star-active{color:var(--comm-color-amber-400)!important}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false} \ No newline at end of file diff --git a/resources/views/comment.blade.php b/resources/views/comment.blade.php index 8c880f3..f27a892 100644 --- a/resources/views/comment.blade.php +++ b/resources/views/comment.blade.php @@ -115,6 +115,21 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1" @else + @if ($comment->isComment() && $comment->rating) +
+ @for ($ratingStar = 1; $ratingStar <= \Kirschbaum\Commentions\Config::getMaxRating(); $ratingStar++) + $ratingStar <= $comment->rating, + 'comm:text-gray-300 comm:dark:text-gray-600' => $ratingStar > $comment->rating, + ]) + /> + @endfor +
+ @endif +
{!! $comment->getParsedBody() !!}
@if ($comment->isComment()) diff --git a/resources/views/comments-modal.blade.php b/resources/views/comments-modal.blade.php index 30560b9..0614ee9 100644 --- a/resources/views/comments-modal.blade.php +++ b/resources/views/comments-modal.blade.php @@ -12,5 +12,7 @@ :sidebar-enabled="$sidebarEnabled ?? true" :show-subscribers="$showSubscribers ?? true" :tip-tap-css-classes="$tipTapCssClasses ?? null" + :ratings-enabled="$ratingsEnabled ?? null" + :max-rating="$maxRating ?? null" /> diff --git a/resources/views/comments.blade.php b/resources/views/comments.blade.php index d151a6f..3d74a54 100644 --- a/resources/views/comments.blade.php +++ b/resources/views/comments.blade.php @@ -5,6 +5,28 @@
@if (Config::resolveAuthenticatedUser()?->can('create', Config::getCommentModel()))
+ @if ($this->ratingsAreEnabled()) +
+ @for ($ratingStar = 1; $ratingStar <= $this->getMaxRating(); $ratingStar++) + + @endfor +
+ + @error('rating') +

{{ $message }}

+ @enderror + @endif + {{-- tiptap editor --}}
diff --git a/src/Actions/SaveComment.php b/src/Actions/SaveComment.php index e4d45ae..2c986a6 100644 --- a/src/Actions/SaveComment.php +++ b/src/Actions/SaveComment.php @@ -16,17 +16,23 @@ class SaveComment /** * @throws AuthorizationException */ - public function __invoke(Model $commentable, Commenter $author, string $body): Comment + public function __invoke(Model $commentable, Commenter $author, string $body, ?int $rating = null): Comment { if ($author->cannot('create', Config::getCommentModel())) { throw new AuthorizationException('Cannot create comment'); } - $comment = $commentable->comments()->create([ + $attributes = [ 'body' => $body, 'author_id' => $author->getKey(), 'author_type' => $author->getMorphClass(), - ]); + ]; + + if ($rating !== null) { + $attributes['rating'] = $rating; + } + + $comment = $commentable->comments()->create($attributes); $this->dispatchEvents($comment); diff --git a/src/Comment.php b/src/Comment.php index 91eca58..4694f65 100644 --- a/src/Comment.php +++ b/src/Comment.php @@ -24,6 +24,7 @@ /** * @property int $id * @property string $body + * @property int|null $rating * @property string $body_markdown * @property string $body_parsed * @property int $author_id @@ -38,10 +39,15 @@ class Comment extends Model implements RenderableComment protected $fillable = [ 'body', + 'rating', 'author_type', 'author_id', ]; + protected $casts = [ + 'rating' => 'integer', + ]; + public function getTable() { return Config::getCommentTable(); diff --git a/src/CommentionsServiceProvider.php b/src/CommentionsServiceProvider.php index 593a66e..50b1850 100644 --- a/src/CommentionsServiceProvider.php +++ b/src/CommentionsServiceProvider.php @@ -40,6 +40,7 @@ public function configurePackage(Package $package): void 'create_commentions_tables', 'create_commentions_reactions_table', 'create_commentions_subscriptions_table', + 'add_rating_to_commentions_comments_table', ]); } diff --git a/src/Config.php b/src/Config.php index 052a2e2..0dc512a 100644 --- a/src/Config.php +++ b/src/Config.php @@ -49,6 +49,11 @@ public static function getCommentSubscriptionTable(): string return config('commentions.tables.comment_subscriptions', 'comment_subscriptions'); } + public static function getMaxRating(): int + { + return (int) config('commentions.ratings.max', 5); + } + public static function resolveCommentUrlUsing(Closure $callback): void { static::$resolveCommentUrl = $callback; diff --git a/src/Filament/Actions/CommentsAction.php b/src/Filament/Actions/CommentsAction.php index 0252ff8..d5bdb95 100644 --- a/src/Filament/Actions/CommentsAction.php +++ b/src/Filament/Actions/CommentsAction.php @@ -7,6 +7,7 @@ use Kirschbaum\Commentions\Filament\Concerns\HasMentionables; use Kirschbaum\Commentions\Filament\Concerns\HasPagination; use Kirschbaum\Commentions\Filament\Concerns\HasPolling; +use Kirschbaum\Commentions\Filament\Concerns\HasRatings; use Kirschbaum\Commentions\Filament\Concerns\HasSidebar; use Kirschbaum\Commentions\Filament\Concerns\HasTipTapCssClasses; @@ -15,6 +16,7 @@ class CommentsAction extends Action use HasMentionables; use HasPagination; use HasPolling; + use HasRatings; use HasSidebar; use HasTipTapCssClasses; @@ -35,6 +37,8 @@ protected function setUp(): void 'sidebarEnabled' => $this->isSidebarEnabled(), 'showSubscribers' => $this->showSubscribers(), 'tipTapCssClasses' => $this->getTipTapCssClasses(), + 'ratingsEnabled' => $this->ratingsAreEnabled(), + 'maxRating' => $this->getMaxRating(), ])) ->modalWidth($this->isSidebarEnabled() ? '4xl' : 'xl') ->label(__('commentions::comments.label')) diff --git a/src/Filament/Actions/CommentsTableAction.php b/src/Filament/Actions/CommentsTableAction.php index d09630b..16e6a15 100644 --- a/src/Filament/Actions/CommentsTableAction.php +++ b/src/Filament/Actions/CommentsTableAction.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Kirschbaum\Commentions\Filament\Concerns\HasMentionables; use Kirschbaum\Commentions\Filament\Concerns\HasPolling; +use Kirschbaum\Commentions\Filament\Concerns\HasRatings; use Kirschbaum\Commentions\Filament\Concerns\HasSidebar; use Kirschbaum\Commentions\Filament\Concerns\HasTipTapCssClasses; @@ -13,6 +14,7 @@ class CommentsTableAction extends Action { use HasMentionables; use HasPolling; + use HasRatings; use HasSidebar; use HasTipTapCssClasses; @@ -29,6 +31,8 @@ protected function setUp(): void 'sidebarEnabled' => $this->isSidebarEnabled(), 'showSubscribers' => $this->showSubscribers(), 'tipTapCssClasses' => $this->getTipTapCssClasses(), + 'ratingsEnabled' => $this->ratingsAreEnabled(), + 'maxRating' => $this->getMaxRating(), ])) ->modalWidth($this->isSidebarEnabled() ? '4xl' : 'xl') ->label(__('commentions::comments.label')) diff --git a/src/Filament/Concerns/HasRatings.php b/src/Filament/Concerns/HasRatings.php new file mode 100644 index 0000000..c45d94d --- /dev/null +++ b/src/Filament/Concerns/HasRatings.php @@ -0,0 +1,45 @@ +ratingsEnabled = $condition; + + return $this; + } + + public function disableRatings(): static + { + $this->ratingsEnabled = false; + + return $this; + } + + public function maxRating(int|Closure $max): static + { + $this->maxRating = $max; + + return $this; + } + + public function ratingsAreEnabled(): bool + { + $value = $this->evaluate($this->ratingsEnabled); + + return $value ?? (bool) config('commentions.ratings.enabled', false); + } + + public function getMaxRating(): int + { + return (int) ($this->evaluate($this->maxRating) ?? config('commentions.ratings.max', 5)); + } +} diff --git a/src/Filament/Infolists/Components/CommentsEntry.php b/src/Filament/Infolists/Components/CommentsEntry.php index 1d6031f..3750663 100644 --- a/src/Filament/Infolists/Components/CommentsEntry.php +++ b/src/Filament/Infolists/Components/CommentsEntry.php @@ -6,6 +6,7 @@ use Kirschbaum\Commentions\Filament\Concerns\HasMentionables; use Kirschbaum\Commentions\Filament\Concerns\HasPagination; use Kirschbaum\Commentions\Filament\Concerns\HasPolling; +use Kirschbaum\Commentions\Filament\Concerns\HasRatings; use Kirschbaum\Commentions\Filament\Concerns\HasSidebar; use Kirschbaum\Commentions\Filament\Concerns\HasTipTapCssClasses; @@ -14,6 +15,7 @@ class CommentsEntry extends Entry use HasMentionables; use HasPagination; use HasPolling; + use HasRatings; use HasSidebar; use HasTipTapCssClasses; diff --git a/src/Livewire/Comments.php b/src/Livewire/Comments.php index fda9a36..3ff8812 100644 --- a/src/Livewire/Comments.php +++ b/src/Livewire/Comments.php @@ -26,6 +26,12 @@ class Comments extends Component public ?string $tipTapCssClasses = null; + public ?bool $ratingsEnabled = null; + + public ?int $maxRating = null; + + public ?int $rating = null; + protected $rules = [ 'commentBody' => 'required|string', ]; @@ -41,10 +47,17 @@ public function save() $this->validate(); + if ($this->ratingsAreEnabled()) { + $this->validate([ + 'rating' => ['nullable', 'integer', 'min:1', 'max:' . $this->getMaxRating()], + ]); + } + SaveComment::run( $this->record, $user, - $this->commentBody + $this->commentBody, + $this->ratingsAreEnabled() ? $this->rating : null, ); $this->clear(); @@ -67,10 +80,21 @@ public function updateCommentBodyContent($value): void public function clear(): void { $this->commentBody = ''; + $this->rating = null; $this->dispatch('comments:content:cleared'); } + public function ratingsAreEnabled(): bool + { + return $this->ratingsEnabled ?? (bool) config('commentions.ratings.enabled', false); + } + + public function getMaxRating(): int + { + return $this->maxRating ?? (int) config('commentions.ratings.max', 5); + } + public function getPlaceholder(): string { return __('commentions::comments.placeholder'); diff --git a/tests/Livewire/CommentRatingTest.php b/tests/Livewire/CommentRatingTest.php new file mode 100644 index 0000000..1ad2b5f --- /dev/null +++ b/tests/Livewire/CommentRatingTest.php @@ -0,0 +1,95 @@ + Auth::user()); +}); + +test('a rating can be attached to a comment when ratings are enabled', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => true]) + ->set('commentBody', 'Great service') + ->set('rating', 4) + ->call('save') + ->assertHasNoErrors(); + + expect(Comment::query()->latest('id')->first()->rating)->toBe(4); +}); + +test('the rating is ignored when ratings are disabled', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post]) + ->set('commentBody', 'No rating here') + ->set('rating', 5) + ->call('save') + ->assertHasNoErrors(); + + expect(Comment::query()->latest('id')->first()->rating)->toBeNull(); +}); + +test('a rating above the configured maximum is rejected', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => true, 'maxRating' => 5]) + ->set('commentBody', 'Too high') + ->set('rating', 9) + ->call('save') + ->assertHasErrors('rating'); + + test()->assertDatabaseCount('comments', 0); +}); + +test('the rating input renders only when ratings are enabled', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => true]) + ->assertSeeHtml('commentions-rating-star'); + + livewire(Comments::class, ['record' => $post]) + ->assertDontSeeHtml('commentions-rating-star'); +}); + +test('CommentsEntry enableRatings and maxRating configure ratings', function () { + expect(CommentsEntry::make('comments')->ratingsAreEnabled())->toBeFalse() + ->and(CommentsEntry::make('comments')->enableRatings()->ratingsAreEnabled())->toBeTrue() + ->and(CommentsEntry::make('comments')->maxRating(10)->getMaxRating())->toBe(10); +}); + +test('a comment renders its star rating', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = Comment::factory()->author($user)->commentable($post)->create([ + 'body' => 'Rated comment', + 'rating' => 3, + ]); + + livewire(CommentComponent::class, ['comment' => $comment]) + ->assertSeeHtml('title="3/5"'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 226f557..323fb7f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -55,6 +55,7 @@ protected function setUpDatabase() __DIR__ . '/../database/migrations/create_commentions_tables.php.stub', __DIR__ . '/../database/migrations/create_commentions_reactions_table.php.stub', __DIR__ . '/../database/migrations/create_commentions_subscriptions_table.php.stub', + __DIR__ . '/../database/migrations/add_rating_to_commentions_comments_table.php.stub', ]; foreach ($migrations as $migration) { From 2539da6ad7b8f82dadae8faf2ebc72f81050a63f Mon Sep 17 00:00:00 2001 From: Malik Alleyne-Jones Date: Sun, 17 May 2026 12:52:25 -0400 Subject: [PATCH 2/2] Add comment ratings feature with per-component configuration --- README.md | 30 +++++ phpstan.neon | 1 - resources/lang/en/comments.php | 4 + resources/views/comment-list.blade.php | 2 + resources/views/comment.blade.php | 13 +- resources/views/comments.blade.php | 22 +--- .../views/partials/rating-input.blade.php | 26 ++++ src/Comment.php | 1 + src/CommentionsServiceProvider.php | 26 ++++ src/Config.php | 5 + src/Filament/Actions/CommentsTableAction.php | 10 +- src/Filament/Actions/TableAction.php | 18 +++ src/Filament/Concerns/HasRatings.php | 7 +- src/Livewire/Comment.php | 20 ++- src/Livewire/CommentList.php | 2 + src/Livewire/Comments.php | 22 +--- src/Livewire/Concerns/HasRatings.php | 22 ++++ tests/Livewire/CommentRatingTest.php | 123 ++++++++++++++++++ 18 files changed, 307 insertions(+), 47 deletions(-) create mode 100644 resources/views/partials/rating-input.blade.php create mode 100644 src/Filament/Actions/TableAction.php create mode 100644 src/Livewire/Concerns/HasRatings.php diff --git a/README.md b/README.md index 354711c..2833c95 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,36 @@ By default, Commentions ships with the following reactions: `['👍', '❤️', ], ``` +#### Comment Ratings + +Commentions can attach an optional star rating to a comment, review-style. Ratings are **disabled by default**. + +Enable them globally in your `config/commentions.php` file (or via the matching environment variables): + +```php + 'ratings' => [ + 'enabled' => env('COMMENTIONS_RATINGS_ENABLED', false), + + 'max' => (int) env('COMMENTIONS_RATINGS_MAX', 5), + ], +``` + +You can also enable ratings per component, which overrides the global config. This works on `CommentsEntry`, `CommentsAction`, and `CommentsTableAction`: + +```php +CommentsEntry::make('comments') + ->enableRatings() + ->maxRating(10) +``` + +Available methods: + +- `enableRatings(bool|Closure $condition = true)` — enable the rating input for this component. +- `disableRatings()` — disable the rating input, even if enabled globally. +- `maxRating(int|Closure $max)` — set the highest selectable rating (defaults to `ratings.max`). + +When ratings are enabled, commenters can pick a rating while writing or editing a comment, and each rated comment renders its score as filled stars. The rating is stored in a nullable `rating` column added by the package's `add_rating_to_commentions_comments_table` migration. + ### Configuring the Commenter name By default, the `name` property will be used to render the mention names. You can customize it either by implementing the Filament `HasName` interface OR by implementing the optional `getCommenterName` method. diff --git a/phpstan.neon b/phpstan.neon index 8eacab5..2572ae3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,7 +9,6 @@ parameters: - tests/ excludePaths: - - src/Filament/Actions/CommentsTableAction.php - src/Filament/Actions/SubscriptionTableAction.php # Level 10 is the highest level diff --git a/resources/lang/en/comments.php b/resources/lang/en/comments.php index 77dc525..d253f70 100644 --- a/resources/lang/en/comments.php +++ b/resources/lang/en/comments.php @@ -21,6 +21,10 @@ 'add_reaction' => 'Add Reaction', 'show_more' => 'Show More', + 'rating_input_label' => 'Rating', + 'rate_stars' => 'Rate :count star|Rate :count stars', + 'rating_display_label' => 'Rated :rating out of :max', + 'notifications' => 'Notifications', 'unsubscribe' => 'Unsubscribe', 'subscribe' => 'Subscribe', diff --git a/resources/views/comment-list.blade.php b/resources/views/comment-list.blade.php index 6ecbf31..9496caf 100644 --- a/resources/views/comment-list.blade.php +++ b/resources/views/comment-list.blade.php @@ -21,6 +21,8 @@ class="comm:w-8 comm:h-8 comm:text-gray-400 comm:dark:text-gray-500" :comment="$comment" :mentionables="$mentionables" :tip-tap-css-classes="$tipTapCssClasses" + :ratings-enabled="$ratingsEnabled" + :max-rating="$maxRating" /> @endforeach diff --git a/resources/views/comment.blade.php b/resources/views/comment.blade.php index f27a892..143cc25 100644 --- a/resources/views/comment.blade.php +++ b/resources/views/comment.blade.php @@ -91,6 +91,10 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1" @if ($editing)
+ @if ($this->ratingsAreEnabled()) + @include('commentions::partials.rating-input', ['maxRating' => $this->getMaxRating()]) + @endif +
@@ -116,8 +120,13 @@ class="comm:text-xs comm:text-gray-300 comm:ml-1"
@else @if ($comment->isComment() && $comment->rating) -
- @for ($ratingStar = 1; $ratingStar <= \Kirschbaum\Commentions\Config::getMaxRating(); $ratingStar++) + diff --git a/resources/views/partials/rating-input.blade.php b/resources/views/partials/rating-input.blade.php new file mode 100644 index 0000000..d4dc501 --- /dev/null +++ b/resources/views/partials/rating-input.blade.php @@ -0,0 +1,26 @@ +
+ @for ($ratingStar = 1; $ratingStar <= $maxRating; $ratingStar++) + + @endfor +
+ +@error('rating') +

{{ $message }}

+@enderror diff --git a/src/Comment.php b/src/Comment.php index 4694f65..11cec02 100644 --- a/src/Comment.php +++ b/src/Comment.php @@ -185,6 +185,7 @@ public function getContentHash(): string { return md5(json_encode([ 'body' => $this->body, + 'rating' => $this->rating, 'reactions' => $this->reactions->pluck('id'), ])); } diff --git a/src/CommentionsServiceProvider.php b/src/CommentionsServiceProvider.php index 50b1850..2dcec1e 100644 --- a/src/CommentionsServiceProvider.php +++ b/src/CommentionsServiceProvider.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\View; use Kirschbaum\Commentions\Comment as CommentModel; use Kirschbaum\Commentions\Events\UserWasMentionedEvent; +use Kirschbaum\Commentions\Filament\Actions\TableAction; use Kirschbaum\Commentions\Listeners\SendUserMentionedNotification; use Kirschbaum\Commentions\Livewire\Comment; use Kirschbaum\Commentions\Livewire\CommentList; @@ -44,6 +45,11 @@ public function configurePackage(Package $package): void ]); } + public function packageRegistered(): void + { + $this->aliasTableAction(); + } + public function packageBooted(): void { $prefix = Config::getComponentPrefix(); @@ -83,4 +89,24 @@ public function packageBooted(): void Event::listen(UserWasMentionedEvent::class, $listenerClass); } } + + /** + * Bridge the package's {@see TableAction} to the correct Filament base class. + * + * Filament 4 unified actions under `Filament\Actions\Action`, removing the + * `Filament\Tables\Actions\Action` base that table actions extend on + * Filament 3. The bundled TableAction class already extends the unified base + * (correct for Filament 4/5); on Filament 3 it must instead resolve to the + * legacy table-action base, so alias it before CommentsTableAction loads. + */ + protected function aliasTableAction(): void + { + if (class_exists(TableAction::class, false)) { + return; + } + + if (class_exists('Filament\Tables\Actions\Action')) { + class_alias('Filament\Tables\Actions\Action', TableAction::class); + } + } } diff --git a/src/Config.php b/src/Config.php index 0dc512a..5f51c28 100644 --- a/src/Config.php +++ b/src/Config.php @@ -49,6 +49,11 @@ public static function getCommentSubscriptionTable(): string return config('commentions.tables.comment_subscriptions', 'comment_subscriptions'); } + public static function ratingsAreEnabled(): bool + { + return (bool) config('commentions.ratings.enabled', false); + } + public static function getMaxRating(): int { return (int) config('commentions.ratings.max', 5); diff --git a/src/Filament/Actions/CommentsTableAction.php b/src/Filament/Actions/CommentsTableAction.php index 16e6a15..b173fac 100644 --- a/src/Filament/Actions/CommentsTableAction.php +++ b/src/Filament/Actions/CommentsTableAction.php @@ -2,7 +2,6 @@ namespace Kirschbaum\Commentions\Filament\Actions; -use Filament\Tables\Actions\Action; use Illuminate\Database\Eloquent\Model; use Kirschbaum\Commentions\Filament\Concerns\HasMentionables; use Kirschbaum\Commentions\Filament\Concerns\HasPolling; @@ -10,7 +9,14 @@ use Kirschbaum\Commentions\Filament\Concerns\HasSidebar; use Kirschbaum\Commentions\Filament\Concerns\HasTipTapCssClasses; -class CommentsTableAction extends Action +/** + * Table/record action for the comments modal. + * + * Filament 3 keeps a dedicated table-action class (`Filament\Tables\Actions\Action`); + * Filament 4/5 unified actions under `Filament\Actions\Action`. {@see TableAction} + * resolves to the correct base class for the installed Filament version. + */ +class CommentsTableAction extends TableAction { use HasMentionables; use HasPolling; diff --git a/src/Filament/Actions/TableAction.php b/src/Filament/Actions/TableAction.php new file mode 100644 index 0000000..8adf8ad --- /dev/null +++ b/src/Filament/Actions/TableAction.php @@ -0,0 +1,18 @@ +evaluate($this->ratingsEnabled); - - return $value ?? (bool) config('commentions.ratings.enabled', false); + return $this->evaluate($this->ratingsEnabled) ?? Config::ratingsAreEnabled(); } public function getMaxRating(): int { - return (int) ($this->evaluate($this->maxRating) ?? config('commentions.ratings.max', 5)); + return (int) ($this->evaluate($this->maxRating) ?? Config::getMaxRating()); } } diff --git a/src/Livewire/Comment.php b/src/Livewire/Comment.php index 82eb5e9..0e6e592 100644 --- a/src/Livewire/Comment.php +++ b/src/Livewire/Comment.php @@ -8,6 +8,7 @@ use Kirschbaum\Commentions\Config; use Kirschbaum\Commentions\Contracts\RenderableComment; use Kirschbaum\Commentions\Livewire\Concerns\HasMentions; +use Kirschbaum\Commentions\Livewire\Concerns\HasRatings; use Livewire\Attributes\On; use Livewire\Attributes\Renderless; use Livewire\Component; @@ -15,6 +16,7 @@ class Comment extends Component { use HasMentions; + use HasRatings; public CommentModel|RenderableComment $comment; @@ -22,6 +24,8 @@ class Comment extends Component public bool $editing = false; + public ?int $rating = null; + public ?string $tipTapCssClasses = null; protected $rules = [ @@ -83,6 +87,7 @@ public function edit(): void $this->editing = true; $this->commentBody = $this->comment->body; + $this->rating = $this->comment->rating; $this->dispatch('comment:updated'); } @@ -93,9 +98,17 @@ public function updateComment() return; } - $this->comment->update([ - 'body' => $this->commentBody, - ]); + $attributes = ['body' => $this->commentBody]; + + if ($this->ratingsAreEnabled()) { + $this->validate([ + 'rating' => ['nullable', 'integer', 'min:1', 'max:' . $this->getMaxRating()], + ]); + + $attributes['rating'] = $this->rating; + } + + $this->comment->update($attributes); $this->editing = false; } @@ -104,6 +117,7 @@ public function cancelEditing() { $this->editing = false; $this->commentBody = ''; + $this->rating = null; } #[Renderless] diff --git a/src/Livewire/CommentList.php b/src/Livewire/CommentList.php index 4f687d7..2370424 100644 --- a/src/Livewire/CommentList.php +++ b/src/Livewire/CommentList.php @@ -7,6 +7,7 @@ use Kirschbaum\Commentions\Livewire\Concerns\HasMentions; use Kirschbaum\Commentions\Livewire\Concerns\HasPagination; use Kirschbaum\Commentions\Livewire\Concerns\HasPolling; +use Kirschbaum\Commentions\Livewire\Concerns\HasRatings; use Livewire\Attributes\Computed; use Livewire\Attributes\On; use Livewire\Component; @@ -16,6 +17,7 @@ class CommentList extends Component use HasMentions; use HasPagination; use HasPolling; + use HasRatings; public Model $record; diff --git a/src/Livewire/Comments.php b/src/Livewire/Comments.php index 3ff8812..e20044e 100644 --- a/src/Livewire/Comments.php +++ b/src/Livewire/Comments.php @@ -8,6 +8,7 @@ use Kirschbaum\Commentions\Livewire\Concerns\HasMentions; use Kirschbaum\Commentions\Livewire\Concerns\HasPagination; use Kirschbaum\Commentions\Livewire\Concerns\HasPolling; +use Kirschbaum\Commentions\Livewire\Concerns\HasRatings; use Kirschbaum\Commentions\Livewire\Concerns\HasSidebar; use Livewire\Attributes\On; use Livewire\Attributes\Renderless; @@ -18,6 +19,7 @@ class Comments extends Component use HasMentions; use HasPagination; use HasPolling; + use HasRatings; use HasSidebar; public Model $record; @@ -26,10 +28,6 @@ class Comments extends Component public ?string $tipTapCssClasses = null; - public ?bool $ratingsEnabled = null; - - public ?int $maxRating = null; - public ?int $rating = null; protected $rules = [ @@ -47,7 +45,9 @@ public function save() $this->validate(); - if ($this->ratingsAreEnabled()) { + $ratingsEnabled = $this->ratingsAreEnabled(); + + if ($ratingsEnabled) { $this->validate([ 'rating' => ['nullable', 'integer', 'min:1', 'max:' . $this->getMaxRating()], ]); @@ -57,7 +57,7 @@ public function save() $this->record, $user, $this->commentBody, - $this->ratingsAreEnabled() ? $this->rating : null, + $ratingsEnabled ? $this->rating : null, ); $this->clear(); @@ -85,16 +85,6 @@ public function clear(): void $this->dispatch('comments:content:cleared'); } - public function ratingsAreEnabled(): bool - { - return $this->ratingsEnabled ?? (bool) config('commentions.ratings.enabled', false); - } - - public function getMaxRating(): int - { - return $this->maxRating ?? (int) config('commentions.ratings.max', 5); - } - public function getPlaceholder(): string { return __('commentions::comments.placeholder'); diff --git a/src/Livewire/Concerns/HasRatings.php b/src/Livewire/Concerns/HasRatings.php new file mode 100644 index 0000000..99cdb76 --- /dev/null +++ b/src/Livewire/Concerns/HasRatings.php @@ -0,0 +1,22 @@ +ratingsEnabled ?? Config::ratingsAreEnabled(); + } + + public function getMaxRating(): int + { + return $this->maxRating ?? Config::getMaxRating(); + } +} diff --git a/tests/Livewire/CommentRatingTest.php b/tests/Livewire/CommentRatingTest.php index 1ad2b5f..86b67f8 100644 --- a/tests/Livewire/CommentRatingTest.php +++ b/tests/Livewire/CommentRatingTest.php @@ -3,6 +3,8 @@ use Illuminate\Support\Facades\Auth; use Kirschbaum\Commentions\Comment; use Kirschbaum\Commentions\Config; +use Kirschbaum\Commentions\Filament\Actions\CommentsAction; +use Kirschbaum\Commentions\Filament\Actions\CommentsTableAction; use Kirschbaum\Commentions\Filament\Infolists\Components\CommentsEntry; use Kirschbaum\Commentions\Livewire\Comment as CommentComponent; use Kirschbaum\Commentions\Livewire\Comments; @@ -93,3 +95,124 @@ livewire(CommentComponent::class, ['comment' => $comment]) ->assertSeeHtml('title="3/5"'); }); + +test('a comment renders its rating against a per-component max rating', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = Comment::factory()->author($user)->commentable($post)->create([ + 'body' => 'Rated comment', + 'rating' => 8, + ]); + + livewire(CommentComponent::class, ['comment' => $comment, 'maxRating' => 10]) + ->assertSeeHtml('title="8/10"'); +}); + +test('a per-component max rating threads through to the rendered comment', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + Comment::factory()->author($user)->commentable($post)->create([ + 'body' => 'Rated comment', + 'rating' => 8, + ]); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => true, 'maxRating' => 10]) + ->assertSeeHtml('title="8/10"'); +}); + +test('a rating below the minimum is rejected', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => true]) + ->set('commentBody', 'Too low') + ->set('rating', 0) + ->call('save') + ->assertHasErrors('rating'); + + test()->assertDatabaseCount('comments', 0); +}); + +test('ratings can be disabled per component even when enabled globally', function () { + config()->set('commentions.ratings.enabled', true); + + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + + livewire(Comments::class, ['record' => $post, 'ratingsEnabled' => false]) + ->set('commentBody', 'No rating') + ->set('rating', 4) + ->call('save') + ->assertHasNoErrors(); + + expect(Comment::query()->latest('id')->first()->rating)->toBeNull(); +}); + +test('CommentsEntry disableRatings overrides the global config', function () { + config()->set('commentions.ratings.enabled', true); + + expect(CommentsEntry::make('comments')->ratingsAreEnabled())->toBeTrue() + ->and(CommentsEntry::make('comments')->disableRatings()->ratingsAreEnabled())->toBeFalse(); +}); + +test('CommentsAction supports rating configuration', function () { + expect(CommentsAction::make()->ratingsAreEnabled())->toBeFalse() + ->and(CommentsAction::make()->enableRatings()->ratingsAreEnabled())->toBeTrue() + ->and(CommentsAction::make()->maxRating(8)->getMaxRating())->toBe(8); +}); + +test('CommentsTableAction supports rating configuration', function () { + expect(CommentsTableAction::make()->ratingsAreEnabled())->toBeFalse() + ->and(CommentsTableAction::make()->enableRatings()->ratingsAreEnabled())->toBeTrue() + ->and(CommentsTableAction::make()->maxRating(8)->getMaxRating())->toBe(8); +}); + +test('a comment rating can be edited when ratings are enabled', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = Comment::factory()->author($user)->commentable($post)->create([ + 'body' => 'Rated comment', + 'rating' => 2, + ]); + + livewire(CommentComponent::class, ['comment' => $comment, 'ratingsEnabled' => true]) + ->call('edit') + ->assertSet('rating', 2) + ->set('rating', 5) + ->call('updateComment') + ->assertHasNoErrors(); + + expect($comment->refresh()->rating)->toBe(5); +}); + +test('editing a comment leaves the rating untouched when ratings are disabled', function () { + $user = User::factory()->create(); + actingAs($user); + + $post = Post::factory()->create(); + $comment = Comment::factory()->author($user)->commentable($post)->create([ + 'body' => 'Rated comment', + 'rating' => 2, + ]); + + livewire(CommentComponent::class, ['comment' => $comment]) + ->call('edit') + ->set('commentBody', 'Edited body') + ->set('rating', 5) + ->call('updateComment') + ->assertHasNoErrors(); + + expect($comment->refresh()) + ->rating->toBe(2) + ->body->toBe('Edited body'); +});