Compare commits
1 Commits
6e10238fe0
...
0.7.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aec460ee4 |
@@ -30,6 +30,7 @@ import { useBulkImport } from "./hooks/use-bulk-import";
|
||||
import { useEncounter } from "./hooks/use-encounter";
|
||||
import { usePlayerCharacters } from "./hooks/use-player-characters";
|
||||
import { useSidePanelState } from "./hooks/use-side-panel-state";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
function rollDice(): number {
|
||||
return Math.floor(Math.random() * 20) + 1;
|
||||
@@ -53,12 +54,12 @@ function useActionBarAnimation(combatantCount: number) {
|
||||
}, [combatantCount]);
|
||||
|
||||
const empty = combatantCount === 0;
|
||||
const risingClass = rising ? " animate-rise-to-center" : "";
|
||||
const settlingClass = settling ? " animate-settle-to-bottom" : "";
|
||||
const risingClass = rising ? "animate-rise-to-center" : "";
|
||||
const settlingClass = settling ? "animate-settle-to-bottom" : "";
|
||||
const exitingClass = topBarExiting
|
||||
? " absolute inset-x-0 top-0 z-10 px-4 animate-slide-up-out"
|
||||
? "absolute inset-x-0 top-0 z-10 px-4 animate-slide-up-out"
|
||||
: "";
|
||||
const topBarClass = settling ? " animate-slide-down-in" : exitingClass;
|
||||
const topBarClass = settling ? "animate-slide-down-in" : exitingClass;
|
||||
const showTopBar = !empty || topBarExiting;
|
||||
|
||||
return {
|
||||
@@ -200,7 +201,7 @@ export function App() {
|
||||
<div className="relative mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-3 px-4">
|
||||
{!!actionBarAnim.showTopBar && (
|
||||
<div
|
||||
className={`shrink-0 pt-8${actionBarAnim.topBarClass}`}
|
||||
className={cn("shrink-0 pt-8", actionBarAnim.topBarClass)}
|
||||
onAnimationEnd={actionBarAnim.onTopBarExitEnd}
|
||||
>
|
||||
<TurnNavigation
|
||||
@@ -216,7 +217,7 @@ export function App() {
|
||||
/* Empty state — ActionBar centered */
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center pt-8 pb-[15%]">
|
||||
<div
|
||||
className={`w-full${actionBarAnim.risingClass}`}
|
||||
className={cn("w-full", actionBarAnim.risingClass)}
|
||||
onAnimationEnd={actionBarAnim.onRiseEnd}
|
||||
>
|
||||
<ActionBar
|
||||
@@ -275,7 +276,7 @@ export function App() {
|
||||
|
||||
{/* Action Bar — fixed at bottom */}
|
||||
<div
|
||||
className={`shrink-0 pb-8${actionBarAnim.settlingClass}`}
|
||||
className={cn("shrink-0 pb-8", actionBarAnim.settlingClass)}
|
||||
onAnimationEnd={actionBarAnim.onSettleEnd}
|
||||
>
|
||||
<ActionBar
|
||||
|
||||
@@ -137,12 +137,14 @@ function AddModeSuggestions({
|
||||
<li key={key}>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${(() => {
|
||||
if (isQueued) return "bg-accent/30 text-foreground";
|
||||
if (i === suggestionIndex)
|
||||
return "bg-accent/20 text-foreground";
|
||||
return "text-foreground hover:bg-hover-neutral-bg";
|
||||
})()}`}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-3 py-1.5 text-left text-foreground text-sm",
|
||||
isQueued && "bg-accent/30",
|
||||
!isQueued && i === suggestionIndex && "bg-accent/20",
|
||||
!isQueued &&
|
||||
i !== suggestionIndex &&
|
||||
"hover:bg-hover-neutral-bg",
|
||||
)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onClickSuggestion(result)}
|
||||
onMouseEnter={() => onSetSuggestionIndex(i)}
|
||||
@@ -502,11 +504,12 @@ export function ActionBar({
|
||||
<li key={creatureKey(result)}>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-3 py-1.5 text-left text-sm",
|
||||
i === suggestionIndex
|
||||
? "bg-accent/20 text-foreground"
|
||||
: "text-foreground hover:bg-hover-neutral-bg"
|
||||
}`}
|
||||
: "text-foreground hover:bg-hover-neutral-bg",
|
||||
)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleBrowseSelect(result)}
|
||||
onMouseEnter={() => setSuggestionIndex(i)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Sparkles,
|
||||
ZapOff,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../lib/utils.js";
|
||||
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
EyeOff,
|
||||
@@ -75,7 +76,10 @@ export function ConditionTags({
|
||||
type="button"
|
||||
title={def.label}
|
||||
aria-label={`Remove ${def.label}`}
|
||||
className={`inline-flex items-center rounded p-0.5 transition-colors hover:bg-hover-neutral-bg ${colorClass}`}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded p-0.5 transition-colors hover:bg-hover-neutral-bg",
|
||||
colorClass,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(condId);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { getSourceDisplayName } from "../adapters/bestiary-index-adapter.js";
|
||||
import type { BulkImportState } from "../hooks/use-bulk-import.js";
|
||||
import { useSwipeToDismiss } from "../hooks/use-swipe-to-dismiss.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { BulkImportPrompt } from "./bulk-import-prompt.js";
|
||||
import { SourceFetchPrompt } from "./source-fetch-prompt.js";
|
||||
import { SourceManager } from "./source-manager.js";
|
||||
@@ -55,9 +56,10 @@ function CollapsedTab({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapse}
|
||||
className={`flex h-full w-[40px] cursor-pointer items-center justify-center text-muted-foreground hover:text-hover-neutral ${
|
||||
side === "right" ? "self-start" : "self-end"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex h-full w-[40px] cursor-pointer items-center justify-center text-muted-foreground hover:text-hover-neutral",
|
||||
side === "right" ? "self-start" : "self-end",
|
||||
)}
|
||||
aria-label="Expand stat block panel"
|
||||
>
|
||||
<span className="writing-vertical-rl font-medium text-sm">
|
||||
@@ -152,7 +154,11 @@ function DesktopPanel({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 bottom-0 flex w-[400px] flex-col border-border bg-card transition-slide-panel ${sideClasses} ${isCollapsed ? collapsedTranslate : "translate-x-0"}`}
|
||||
className={cn(
|
||||
"fixed top-0 bottom-0 flex w-[400px] flex-col border-border bg-card transition-slide-panel",
|
||||
sideClasses,
|
||||
isCollapsed ? collapsedTranslate : "translate-x-0",
|
||||
)}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<CollapsedTab
|
||||
@@ -194,7 +200,10 @@ function MobileDrawer({
|
||||
aria-label="Close stat block"
|
||||
/>
|
||||
<div
|
||||
className={`absolute top-0 right-0 bottom-0 w-[85%] max-w-md border-border border-l bg-card shadow-xl ${isSwiping ? "" : "animate-slide-in-right"}`}
|
||||
className={cn(
|
||||
"absolute top-0 right-0 bottom-0 w-[85%] max-w-md border-border border-l bg-card shadow-xl",
|
||||
!isSwiping && "animate-slide-in-right",
|
||||
)}
|
||||
style={
|
||||
isSwiping ? { transform: `translateX(${offsetX}px)` } : undefined
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"jscpd": "jscpd",
|
||||
"oxlint": "oxlint --tsconfig apps/web/tsconfig.json --type-aware",
|
||||
"check:ignores": "node scripts/check-lint-ignores.mjs",
|
||||
"check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && node scripts/check-lint-ignores.mjs && tsc --build && vitest run && jscpd"
|
||||
"check:classnames": "node scripts/check-cn-classnames.mjs",
|
||||
"check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && node scripts/check-lint-ignores.mjs && node scripts/check-cn-classnames.mjs && tsc --build && vitest run && jscpd"
|
||||
}
|
||||
}
|
||||
|
||||
47
scripts/check-cn-classnames.mjs
Normal file
47
scripts/check-cn-classnames.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Ban template-literal classNames in TSX files.
|
||||
*
|
||||
* Tailwind v4's production content extractor does static analysis on source
|
||||
* files to discover utility classes. Template literals like
|
||||
* className={`foo ${bar}`}
|
||||
* can cause the extractor to miss classes adjacent to `${`, leading to
|
||||
* styles that work in dev (JIT) but break in production.
|
||||
*
|
||||
* Rule: always use cn() for dynamic class composition instead.
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const PATTERN = /className\s*=\s*\{`/;
|
||||
|
||||
function findFiles() {
|
||||
return execSync("git ls-files -- '*.tsx'", { encoding: "utf-8" })
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
let errors = 0;
|
||||
|
||||
for (const file of findFiles()) {
|
||||
const lines = readFileSync(file, "utf-8").split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (PATTERN.test(lines[i])) {
|
||||
console.error(
|
||||
`${file}:${i + 1}: className uses template literal — use cn() instead`,
|
||||
);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors > 0) {
|
||||
console.error(
|
||||
`\n${errors} template-literal className(s) found. Use cn() for dynamic classes.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("No template-literal classNames found.");
|
||||
}
|
||||
Reference in New Issue
Block a user