diff --git a/app/registry/blocks/field/Field.vue b/app/registry/blocks/field/Field.vue
new file mode 100644
index 0000000..5ff6cb4
--- /dev/null
+++ b/app/registry/blocks/field/Field.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldContent.vue b/app/registry/blocks/field/FieldContent.vue
new file mode 100644
index 0000000..70d5d7e
--- /dev/null
+++ b/app/registry/blocks/field/FieldContent.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldDescription.vue b/app/registry/blocks/field/FieldDescription.vue
new file mode 100644
index 0000000..b46304f
--- /dev/null
+++ b/app/registry/blocks/field/FieldDescription.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldError.vue b/app/registry/blocks/field/FieldError.vue
new file mode 100644
index 0000000..6d918cb
--- /dev/null
+++ b/app/registry/blocks/field/FieldError.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ {{ messages[0] }}
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldGroup.vue b/app/registry/blocks/field/FieldGroup.vue
new file mode 100644
index 0000000..1e25ab1
--- /dev/null
+++ b/app/registry/blocks/field/FieldGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldLabel.vue b/app/registry/blocks/field/FieldLabel.vue
new file mode 100644
index 0000000..6f37231
--- /dev/null
+++ b/app/registry/blocks/field/FieldLabel.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldLegend.vue b/app/registry/blocks/field/FieldLegend.vue
new file mode 100644
index 0000000..11a1fad
--- /dev/null
+++ b/app/registry/blocks/field/FieldLegend.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldSeparator.vue b/app/registry/blocks/field/FieldSeparator.vue
new file mode 100644
index 0000000..5524e1f
--- /dev/null
+++ b/app/registry/blocks/field/FieldSeparator.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldSet.vue b/app/registry/blocks/field/FieldSet.vue
new file mode 100644
index 0000000..52bfee5
--- /dev/null
+++ b/app/registry/blocks/field/FieldSet.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/registry/blocks/field/FieldTitle.vue b/app/registry/blocks/field/FieldTitle.vue
new file mode 100644
index 0000000..ce620fa
--- /dev/null
+++ b/app/registry/blocks/field/FieldTitle.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/registry/blocks/field/index.ts b/app/registry/blocks/field/index.ts
new file mode 100644
index 0000000..cf2450a
--- /dev/null
+++ b/app/registry/blocks/field/index.ts
@@ -0,0 +1,10 @@
+export { default as Field } from "./Field.vue";
+export { default as FieldContent } from "./FieldContent.vue";
+export { default as FieldDescription } from "./FieldDescription.vue";
+export { default as FieldError, type FieldErrorItem } from "./FieldError.vue";
+export { default as FieldGroup } from "./FieldGroup.vue";
+export { default as FieldLabel } from "./FieldLabel.vue";
+export { default as FieldLegend } from "./FieldLegend.vue";
+export { default as FieldSeparator } from "./FieldSeparator.vue";
+export { default as FieldSet } from "./FieldSet.vue";
+export { default as FieldTitle } from "./FieldTitle.vue";
diff --git a/content/docs/2.components/18.form.md b/content/docs/2.components/18.form.md
index 6d6f15a..c8625e1 100644
--- a/content/docs/2.components/18.form.md
+++ b/content/docs/2.components/18.form.md
@@ -14,11 +14,10 @@ name: FormDemo
## Installation
-Add the shadcn-vue controls you want to compose, then add Stackhacker UI's form layer.
+Add the shadcn-vue controls you want to compose, then add Stackhacker UI's form layer. `form-field` installs the Stackhacker UI `field` primitive and `form` dependency automatically.
```bash
-npx shadcn-vue@latest add field input textarea select
-npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form.json"
+npx shadcn-vue@latest add input textarea select
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form-field.json"
```
diff --git a/content/docs/2.components/19.form-field.md b/content/docs/2.components/19.form-field.md
index 110ffe8..4b0e9b8 100644
--- a/content/docs/2.components/19.form-field.md
+++ b/content/docs/2.components/19.form-field.md
@@ -13,7 +13,7 @@ name: FormFieldDemo
## Installation
```bash
-npx shadcn-vue@latest add field input
+npx shadcn-vue@latest add input
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/form-field.json"
```
diff --git a/scripts/registry-build.ts b/scripts/registry-build.ts
index dbc3d54..0b41f85 100644
--- a/scripts/registry-build.ts
+++ b/scripts/registry-build.ts
@@ -1,6 +1,6 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
-import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
+import { cp, mkdir, readFile, rm, rmdir, unlink, writeFile } from "node:fs/promises";
import pkg from "../package.json";
import components from "../components.json";
import { generateShadcnRegistry } from "shadcn-vue-registry";
@@ -13,6 +13,10 @@ import { x } from "tinyexec";
const cwd = resolve(__dirname, "../");
const registryPath = resolve(cwd, "./app/registry");
const outputPath = resolve(cwd, "./public/r/");
+ const fieldBlockPath = resolve(registryPath, "blocks/field");
+ const fieldUiPath = resolve(registryPath, "components/ui/field");
+ const fieldUiParentPath = resolve(registryPath, "components/ui");
+ const fieldComponentsPath = resolve(registryPath, "components");
const config = {
root: cwd,
name: pkg.name,
@@ -27,12 +31,26 @@ import { x } from "tinyexec";
const customItemNames = new Set(registryJson.items.map(item => item.name));
const itemPackageDependencies: Record = {
"chat-message": ["@lucide/vue"],
+ "field": ["@vueuse/core", "reka-ui"],
};
for (const item of registryJson.items) {
const dependencies = new Set(item.dependencies ?? []);
const registryDependencies = new Set(item.registryDependencies ?? []);
+ if (item.name === "field") {
+ await rm(fieldUiPath, { recursive: true, force: true });
+ await mkdir(fieldUiPath, { recursive: true });
+ await cp(fieldBlockPath, fieldUiPath, { recursive: true });
+
+ item.type = "registry:ui";
+ item.files = item.files.map(file => ({
+ ...file,
+ path: file.path.replace("blocks/field", "components/ui/field"),
+ type: "registry:ui",
+ }));
+ }
+
for (const dependency of itemPackageDependencies[item.name] ?? []) {
dependencies.add(dependency);
}
@@ -46,6 +64,7 @@ import { x } from "tinyexec";
if (
content.includes(`../${dependency}`)
|| content.includes(`@/components/${dependency}`)
+ || content.includes(`@/components/ui/${dependency}`)
) {
registryDependencies.add(dependency);
}
@@ -67,14 +86,20 @@ import { x } from "tinyexec";
await mkdir(outputPath, { recursive: true });
console.log(`ā Output directory created: ${outputPath}`);
- await x("shadcn-vue", ["build", "-c", registryPath, "-o", outputPath], {
- nodeOptions: {
- cwd,
- shell: true,
- },
- });
-
- await unlink(registryJsonPath);
+ try {
+ await x("shadcn-vue", ["build", "-c", registryPath, "-o", outputPath], {
+ nodeOptions: {
+ cwd,
+ shell: true,
+ },
+ });
+ }
+ finally {
+ await unlink(registryJsonPath).catch(() => {});
+ await rm(fieldUiPath, { recursive: true, force: true });
+ await rmdir(fieldUiParentPath).catch(() => {});
+ await rmdir(fieldComponentsPath).catch(() => {});
+ }
console.log("\nā Registry build complete");
console.log("Registry files available at:\n /r/registry.json\n /r/{name}.json");
diff --git a/scripts/registry-verify.ts b/scripts/registry-verify.ts
index 08ffc6d..5a048f7 100644
--- a/scripts/registry-verify.ts
+++ b/scripts/registry-verify.ts
@@ -4,8 +4,13 @@ import registryConfig from "../registry.config";
interface RegistryItem {
name: string;
+ type?: string;
dependencies?: string[];
registryDependencies?: string[];
+ files?: Array<{
+ path?: string;
+ type?: string;
+ }>;
}
interface RegistryIndex {
@@ -66,6 +71,27 @@ function assertExactValues(itemName: string, fieldName: string, actual: string[]
}
}
+function assertFieldRegistryUiOutput(item: RegistryItem) {
+ if (item.type !== "registry:ui") {
+ errors.push(`field: expected type registry:ui, received ${item.type ?? "missing"}`);
+ }
+
+ if (!item.files?.length) {
+ errors.push("field: expected registry files under components/ui/field");
+ return;
+ }
+
+ for (const file of item.files) {
+ if (file.type !== "registry:ui") {
+ errors.push(`field: expected file ${file.path ?? "missing"} type registry:ui, received ${file.type ?? "missing"}`);
+ }
+
+ if (!file.path?.startsWith("components/ui/field/")) {
+ errors.push(`field: expected file path under components/ui/field, received ${file.path ?? "missing"}`);
+ }
+ }
+}
+
const registryIndex = await readRegistryIndex();
if (registryIndex?.homepage !== expectedHomepage) {
errors.push(`registry: expected homepage ${expectedHomepage}, received ${registryIndex?.homepage ?? "missing"}`);
@@ -167,9 +193,13 @@ const expectedItems: Record = {
dependencies: [],
registryDependencies: [],
},
+ "field": {
+ dependencies: ["@vueuse/core", "reka-ui"],
+ registryDependencies: [],
+ },
"form-field": {
dependencies: [],
- registryDependencies: ["field", stackhackerDependency("form")],
+ registryDependencies: [stackhackerDependency("field"), stackhackerDependency("form")],
},
};
@@ -196,6 +226,10 @@ for (const [name, expected] of Object.entries(expectedItems)) {
assertExactValues(name, "package dependency", item.dependencies ?? [], expected.dependencies ?? []);
assertExactValues(name, "registry dependency", item.registryDependencies ?? [], expected.registryDependencies ?? []);
+
+ if (name === "field") {
+ assertFieldRegistryUiOutput(item);
+ }
}
if (errors.length > 0) {