diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
index 6eed5d3..1c50425 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.gitea/workflows/ci.yaml
@@ -27,3 +27,4 @@ jobs:
- run: pnpm test
- run: pnpm build
- run: pnpm check:size
+ - run: pnpm check:colors
diff --git a/web/package.json b/web/package.json
index be54783..3ad401b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -14,6 +14,7 @@
"lint": "eslint .",
"gen:api": "openapi-typescript http://localhost:8080/api-docs/openapi.json -o src/api/schema.d.ts",
"check:size": "node scripts/check-bundle-size.mjs",
+ "check:colors": "node scripts/check-no-raw-colors.mjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
diff --git a/web/scripts/check-no-raw-colors.mjs b/web/scripts/check-no-raw-colors.mjs
new file mode 100644
index 0000000..e2bc9d6
--- /dev/null
+++ b/web/scripts/check-no-raw-colors.mjs
@@ -0,0 +1,42 @@
+// Fails if any raw Tailwind color utility appears outside src/components/ui/.
+import { readdirSync, readFileSync } from "node:fs";
+import { join, relative } from "node:path";
+
+const root = "src";
+const excludeDir = join("src", "components", "ui");
+const RAW_COLOR =
+ /(?:text|bg|border|ring|fill|stroke|from|to|via|decoration|outline|divide|placeholder)-(?:neutral|gray|slate|zinc|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)\b/;
+
+function walk(dir) {
+ const files = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const path = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ if (path === excludeDir) continue;
+ files.push(...walk(path));
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
+ files.push(path);
+ }
+ }
+ return files;
+}
+
+const files = walk(root);
+const offenses = [];
+for (const file of files) {
+ const lines = readFileSync(file, "utf8").split("\n");
+ for (let i = 0; i < lines.length; i++) {
+ const match = RAW_COLOR.exec(lines[i]);
+ if (match) offenses.push(`${relative(".", file)}:${i + 1}: ${match[0]}`);
+ }
+}
+
+if (offenses.length > 0) {
+ console.error(
+ `raw color utilities found outside components/ui/ (${offenses.length}):`,
+ );
+ for (const offense of offenses) console.error(` ${offense}`);
+ process.exit(1);
+}
+
+console.log(`no raw color utilities outside components/ui/ (${files.length} files scanned)`);
diff --git a/web/src/app.tsx b/web/src/app.tsx
index 54205c4..a5e58a9 100644
--- a/web/src/app.tsx
+++ b/web/src/app.tsx
@@ -26,7 +26,7 @@ const FieldsPage = lazy(() =>
);
function FormFallback() {
- return
Loading…
;
+ return Loading…
;
}
export function App() {
diff --git a/web/src/auth/login-page.tsx b/web/src/auth/login-page.tsx
index 32b9e18..6009685 100644
--- a/web/src/auth/login-page.tsx
+++ b/web/src/auth/login-page.tsx
@@ -53,7 +53,7 @@ export function LoginPage() {
/>
{errorKey && (
-
+
{t(errorKey)}
)}
diff --git a/web/src/authorities/authorities-page.tsx b/web/src/authorities/authorities-page.tsx
index 676f92c..b0696af 100644
--- a/web/src/authorities/authorities-page.tsx
+++ b/web/src/authorities/authorities-page.tsx
@@ -53,7 +53,7 @@ export function AuthoritiesPage() {
role="tab"
aria-selected={k === currentKind}
className={({ isActive }) =>
- `rounded px-3 py-1 text-sm ${isActive ? "bg-neutral-800 text-white" : "border"}`
+ `rounded-md px-3 py-1 text-sm ${isActive ? "bg-primary text-primary-foreground" : "border"}`
}
>
{t(`authorities.${k}`)}
@@ -63,13 +63,13 @@ export function AuthoritiesPage() {
{isLoading && (
- - …
+ - …
)}
{isError && (
- - {t("authorities.loadError")}
+ - {t("authorities.loadError")}
)}
{!isLoading && !isError && authorities?.length === 0 && (
- - {t("authorities.empty")}
+ - {t("authorities.empty")}
)}
{authorities?.map((a) => (
@@ -84,13 +84,13 @@ export function AuthoritiesPage() {
{error && (
-
+
{t("form.required")}
)}
{create.isError && (
-
+
{t("form.rejected")}
)}
diff --git a/web/src/components/delete-confirm-dialog.tsx b/web/src/components/delete-confirm-dialog.tsx
index 190d2ff..43dcd62 100644
--- a/web/src/components/delete-confirm-dialog.tsx
+++ b/web/src/components/delete-confirm-dialog.tsx
@@ -47,7 +47,7 @@ export function DeleteConfirmDialog({
+
}
@@ -56,7 +56,7 @@ export function DeleteConfirmDialog({
{t("actions.delete")}
{description}
{message && (
-
+
{message}
)}
diff --git a/web/src/components/ui/badge.stories.tsx b/web/src/components/ui/badge.stories.tsx
index e8676a8..cc2974a 100644
--- a/web/src/components/ui/badge.stories.tsx
+++ b/web/src/components/ui/badge.stories.tsx
@@ -27,6 +27,14 @@ export const Destructive: Story = {
args: { variant: 'destructive', children: 'Error' },
}
+export const Success: Story = {
+ args: { variant: 'success', children: 'Public' },
+}
+
+export const Warning: Story = {
+ args: { variant: 'warning', children: 'Internal' },
+}
+
export const Outline: Story = {
args: { variant: 'outline', children: 'Draft' },
}
diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx
index b20959d..c4bdbcd 100644
--- a/web/src/components/ui/badge.tsx
+++ b/web/src/components/ui/badge.tsx
@@ -14,6 +14,8 @@ const badgeVariants = cva(
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
+ success: "bg-success/10 text-success [a]:hover:bg-success/20",
+ warning: "bg-warning/10 text-warning [a]:hover:bg-warning/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
diff --git a/web/src/fields/field-form.tsx b/web/src/fields/field-form.tsx
index c153128..80da315 100644
--- a/web/src/fields/field-form.tsx
+++ b/web/src/fields/field-form.tsx
@@ -122,7 +122,7 @@ export function FieldForm({
value={dataType}
disabled={isEdit}
onChange={(e) => setDataType(e.target.value)}
- className="w-full rounded border px-2 py-1 text-sm disabled:opacity-60"
+ className="w-full rounded-md border px-2 py-1 text-sm disabled:opacity-60"
>
{TYPES.map((type) => (
{vocabularies?.map((vocab) => (
@@ -160,7 +160,7 @@ export function FieldForm({
value={authorityKind}
disabled={isEdit}
onChange={(e) => setAuthorityKind(e.target.value)}
- className="w-full rounded border px-2 py-1 text-sm disabled:opacity-60"
+ className="w-full rounded-md border px-2 py-1 text-sm disabled:opacity-60"
>
{KINDS.map((kind) => (
@@ -183,12 +183,12 @@ export function FieldForm({
{error && (
-
+
{t("form.required")}
)}
{failed && (
-
+
{t("form.rejected")}
)}
diff --git a/web/src/fields/field-list.tsx b/web/src/fields/field-list.tsx
index 320cbb8..2c43168 100644
--- a/web/src/fields/field-list.tsx
+++ b/web/src/fields/field-list.tsx
@@ -30,9 +30,9 @@ export function FieldList({
);
}
- if (isError) return {t("fields.loadError")}
;
+ if (isError) return {t("fields.loadError")}
;
if (!data || data.length === 0)
- return {t("fields.empty")}
;
+ return {t("fields.empty")}
;
const groups = new Map();
@@ -53,7 +53,7 @@ export function FieldList({
{entries.map(([group, defs]) => (
-
-
+
{group}
@@ -61,7 +61,7 @@ export function FieldList({
-