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 @@ + + + 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) {