Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/registry/blocks/field/Field.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = withDefaults(defineProps<{
orientation?: "vertical" | "horizontal" | "responsive";
class?: HTMLAttributes["class"];
}>(), {
orientation: "vertical",
});

defineOptions({
name: "StackhackerField",
});
</script>

<template>
<div
data-slot="field"
:data-orientation="orientation"
:class="cn(
'grid gap-2 data-[orientation=horizontal]:grid-cols-[auto_1fr] data-[orientation=horizontal]:items-center data-[orientation=responsive]:gap-3 @md/field-group:data-[orientation=responsive]:grid-cols-[auto_1fr] @md/field-group:data-[orientation=responsive]:items-center',
props.class,
)"
role="group"
>
<slot />
</div>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<div
data-slot="field-content"
:class="cn('grid gap-1.5 leading-none', props.class)"
>
<slot />
</div>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<p
data-slot="field-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
47 changes: 47 additions & 0 deletions app/registry/blocks/field/FieldError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { computed } from "vue";
import { cn } from "@/lib/utils";

export interface FieldErrorItem {
message?: string;
}

const props = defineProps<{
error?: string | boolean | null;
errors?: Array<string | FieldErrorItem | undefined> | null;
class?: HTMLAttributes["class"];
}>();

const messages = computed(() => {
if (typeof props.error === "string") return [props.error];
return (props.errors ?? [])
.map(error => typeof error === "string" ? error : error?.message)
.filter((message): message is string => Boolean(message));
});
</script>

<template>
<div
v-if="messages.length || $slots.default"
data-slot="field-error"
:class="cn('text-destructive text-sm font-medium', props.class)"
>
<slot>
<p v-if="messages.length === 1">
{{ messages[0] }}
</p>
<ul
v-else
class="list-disc ps-5"
>
<li
v-for="message in messages"
:key="message"
>
{{ message }}
</li>
</ul>
</slot>
</div>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<div
data-slot="field-group"
:class="cn('@container/field-group grid gap-6', props.class)"
>
<slot />
</div>
</template>
20 changes: 20 additions & 0 deletions app/registry/blocks/field/FieldLabel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { LabelProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { Label } from "reka-ui";
import { cn } from "@/lib/utils";

const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = reactiveOmit(props, "class");
</script>

<template>
<Label
data-slot="field-label"
v-bind="delegatedProps"
:class="cn('flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50', props.class)"
>
<slot />
</Label>
</template>
21 changes: 21 additions & 0 deletions app/registry/blocks/field/FieldLegend.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = withDefaults(defineProps<{
variant?: "legend" | "label";
class?: HTMLAttributes["class"];
}>(), {
variant: "legend",
});
</script>

<template>
<legend
data-slot="field-legend"
:data-variant="variant"
:class="cn('mb-3 font-medium data-[variant=legend]:text-base data-[variant=label]:text-sm', props.class)"
>
<slot />
</legend>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldSeparator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<div
data-slot="field-separator"
:class="cn('bg-border h-px w-full', props.class)"
>
<slot />
</div>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldSet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<fieldset
data-slot="field-set"
:class="cn('grid gap-6', props.class)"
>
<slot />
</fieldset>
</template>
15 changes: 15 additions & 0 deletions app/registry/blocks/field/FieldTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>

<template>
<div
data-slot="field-title"
:class="cn('text-sm leading-none font-medium', props.class)"
>
<slot />
</div>
</template>
10 changes: 10 additions & 0 deletions app/registry/blocks/field/index.ts
Original file line number Diff line number Diff line change
@@ -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";
5 changes: 2 additions & 3 deletions content/docs/2.components/18.form.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```

Expand Down
2 changes: 1 addition & 1 deletion content/docs/2.components/19.form-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```

Expand Down
43 changes: 34 additions & 9 deletions scripts/registry-build.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -27,12 +31,26 @@ import { x } from "tinyexec";
const customItemNames = new Set(registryJson.items.map(item => item.name));
const itemPackageDependencies: Record<string, string[]> = {
"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);
}
Expand All @@ -46,6 +64,7 @@ import { x } from "tinyexec";
if (
content.includes(`../${dependency}`)
|| content.includes(`@/components/${dependency}`)
|| content.includes(`@/components/ui/${dependency}`)
) {
registryDependencies.add(dependency);
}
Expand All @@ -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");
Expand Down
Loading
Loading