diff --git a/demo/index.html b/demo/index.html
index 408b731..a943ac6 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -20,7 +20,9 @@
@@include('./partials/utilities.html')
@@include('./partials/buttons.html')
@@include('./partials/choices.html')
+ @@include('./partials/select-textarea.html')
@@include('./partials/inputs.html')
+ @@include('./partials/cards.html')
@@include('./partials/badges.html')
@@include('./partials/alerts.html')
diff --git a/demo/partials/cards.html b/demo/partials/cards.html
new file mode 100644
index 0000000..7a6fb4f
--- /dev/null
+++ b/demo/partials/cards.html
@@ -0,0 +1,68 @@
+
+ Cards
+
+
+
+
+
+ A simple card with title, content, and footer.
+
+
+
+
+
+
+
+ Uses a subtle background fill instead of white.
+
+
+
+
+
+
+
+ Transparent background with a stronger border.
+
+
+
+
+
+
+
+
+ Primary accent card with yellow tint.
+
+
+
+
+
+ Secondary accent card with mint tint.
+
+
+
+
+
+ Elevated card with a soft shadow.
+
+
+
+
+ Sizes
+
+
+
+ Compact padding and radius.
+
+
+
+
+ Generous padding and larger radius.
+
+
+
diff --git a/demo/partials/select-textarea.html b/demo/partials/select-textarea.html
new file mode 100644
index 0000000..9d6f7ca
--- /dev/null
+++ b/demo/partials/select-textarea.html
@@ -0,0 +1,38 @@
+
+ Select & Textarea
+
+ Select
+
+
+ Textarea
+
+
diff --git a/docs/component-coverage.md b/docs/component-coverage.md
index 30d32ae..ee0574e 100644
--- a/docs/component-coverage.md
+++ b/docs/component-coverage.md
@@ -20,9 +20,10 @@
| Alert | done | GnAlert | partial |
| Checkbox | done | GnCheckbox | partial |
| Radio | done | GnRadio, GnRadioGroup | partial |
-| Select | — | — | — |
+| Select | done | GnSelect | partial |
| Switch | done | GnSwitch | partial |
-| Card | — | — | — |
+| Textarea | done | GnTextarea | partial |
+| Card | done | GnCard | partial |
| Modal | — | — | — |
| Drawer | — | — | — |
| Toast | — | — | — |
diff --git a/examples/vue/src/main.js b/examples/vue/src/main.js
index fa69035..87322cc 100644
--- a/examples/vue/src/main.js
+++ b/examples/vue/src/main.js
@@ -41,6 +41,15 @@
{ value: 'b', label: 'Option B' }
]" />
+
+
+ Done
+
+
+
`
};
diff --git a/src/scss/components/_cards.scss b/src/scss/components/_cards.scss
new file mode 100644
index 0000000..26c5fc3
--- /dev/null
+++ b/src/scss/components/_cards.scss
@@ -0,0 +1,90 @@
+/* =========================
+ Cards
+========================= */
+
+@use "../kit-deps" as *;
+
+.card {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ background-color: $neutral-0;
+ border: 1px solid $neutral-200;
+ border-radius: $radius-lg;
+ overflow: hidden;
+ transition:
+ box-shadow $motion-base $motion-ease,
+ border-color $motion-base $motion-ease;
+
+ &:hover {
+ border-color: $neutral-300;
+ }
+
+ .card-title {
+ padding: $space-4 $space-5;
+ font-size: 18px;
+ font-weight: $font-weight-bold;
+ color: $neutral-900;
+ border-bottom: 1px solid $neutral-100;
+ }
+
+ .card-content {
+ flex: 1;
+ padding: $space-5;
+ color: $neutral-800;
+ font-size: 15px;
+ line-height: 1.5;
+ }
+
+ .card-footer {
+ display: flex;
+ align-items: center;
+ gap: $space-3;
+ padding: $space-3 $space-5;
+ border-top: 1px solid $neutral-100;
+ background-color: $neutral-50;
+ }
+
+ // Variants
+ &.card-filled {
+ background-color: $neutral-50;
+ border-color: $neutral-100;
+ }
+
+ &.card-outlined {
+ background-color: transparent;
+ border-color: $neutral-400;
+ }
+
+ &.card-primary {
+ border-color: $yellow-300;
+ background-color: $yellow-50;
+ }
+
+ &.card-secondary {
+ border-color: $mint-300;
+ background-color: $mint-50;
+ }
+
+ &.card-elevated {
+ box-shadow: 0 4px 24px rgba($neutral-900, 0.08);
+ border-color: transparent;
+ }
+
+ // Sizes
+ &.card-sm {
+ border-radius: $radius-md;
+
+ .card-title { padding: $space-3 $space-4; font-size: 16px; }
+ .card-content { padding: $space-4; }
+ .card-footer { padding: $space-2 $space-4; }
+ }
+
+ &.card-lg {
+ border-radius: $radius-xl;
+
+ .card-title { padding: $space-5 $space-6; font-size: 20px; }
+ .card-content { padding: $space-6; }
+ .card-footer { padding: $space-4 $space-6; }
+ }
+}
diff --git a/src/scss/components/_forms.scss b/src/scss/components/_forms.scss
index 5950600..657e4f7 100644
--- a/src/scss/components/_forms.scss
+++ b/src/scss/components/_forms.scss
@@ -138,6 +138,14 @@
}
}
+.form-group {
+ textarea.input {
+ min-height: 120px;
+ resize: vertical;
+ line-height: 1.4;
+ }
+}
+
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
diff --git a/src/scss/components/_select.scss b/src/scss/components/_select.scss
new file mode 100644
index 0000000..f41f4c8
--- /dev/null
+++ b/src/scss/components/_select.scss
@@ -0,0 +1,42 @@
+/* =========================
+ Select
+========================= */
+
+@use "../kit-deps" as *;
+
+.select-wrap {
+ position: relative;
+ width: 100%;
+
+ &::after {
+ content: "";
+ position: absolute;
+ right: $space-4;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 10px;
+ height: 10px;
+ border: solid $neutral-700;
+ border-width: 0 2px 2px 0;
+ transform: translateY(-70%) rotate(45deg);
+ pointer-events: none;
+ }
+
+ .select {
+ width: 100%;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ padding-right: $space-8;
+ cursor: pointer;
+
+ &::-ms-expand {
+ display: none;
+ }
+
+ option {
+ color: $neutral-900;
+ background-color: $neutral-0;
+ }
+ }
+}
diff --git a/src/scss/kit.scss b/src/scss/kit.scss
index 98f307f..48f9c0f 100644
--- a/src/scss/kit.scss
+++ b/src/scss/kit.scss
@@ -6,9 +6,11 @@
@use "components/badges";
@use "components/alerts";
@use "components/forms";
+@use "components/select";
@use "components/checkbox";
@use "components/radio";
@use "components/switch";
+@use "components/cards";
@use "utils";
* {
diff --git a/src/vue/components/GnCard.js b/src/vue/components/GnCard.js
new file mode 100644
index 0000000..e3e7433
--- /dev/null
+++ b/src/vue/components/GnCard.js
@@ -0,0 +1,30 @@
+import { defineComponent, h } from "vue";
+import { cx } from "../utils.js";
+
+export default defineComponent({
+ name: "GnCard",
+ inheritAttrs: false,
+ props: {
+ title: { type: String, default: "" },
+ variant: { type: String, default: "" },
+ size: { type: String, default: "" }
+ },
+ setup(props, { attrs, slots }) {
+ return () => h("article", {
+ ...attrs,
+ class: cx(
+ "card",
+ props.variant && `card-${props.variant}`,
+ {
+ "card-sm": props.size === "sm",
+ "card-lg": props.size === "lg"
+ },
+ attrs.class
+ )
+ }, [
+ (props.title || slots.title) && h("header", { class: "card-title" }, slots.title?.() || props.title),
+ h("div", { class: "card-content" }, slots.default?.()),
+ slots.footer && h("footer", { class: "card-footer" }, slots.footer())
+ ]);
+ }
+});
diff --git a/src/vue/components/GnSelect.js b/src/vue/components/GnSelect.js
new file mode 100644
index 0000000..fce11de
--- /dev/null
+++ b/src/vue/components/GnSelect.js
@@ -0,0 +1,64 @@
+/**
+ * GnSelect - Dropdown select with label, icon, state, and help text.
+ *
+ * @typedef {Object} GnSelectProps
+ * @property {string|number} [modelValue=''] - Bound value
+ * @property {string} [label=''] - Label text
+ * @property {string} [icon=''] - Phosphor icon name with ph- prefix or VMK key
+ * @property {string} [state=''] - error | warning | success
+ * @property {string} [help=''] - Help or validation message
+ * @property {Array} [options=[]] - Array of strings or { value, label } objects
+ *
+ * @slots default - Override option nodes
+ * @emits update:modelValue
+ */
+
+import { defineComponent, h } from "vue";
+import { cx, eventValue, iconNode, normalizeInputState } from "../utils.js";
+
+export default defineComponent({
+ name: "GnSelect",
+ inheritAttrs: false,
+ props: {
+ modelValue: { type: [String, Number], default: "" },
+ label: { type: String, default: "" },
+ icon: { type: String, default: "" },
+ state: { type: String, default: "" },
+ help: { type: String, default: "" },
+ options: { type: Array, default: () => [] }
+ },
+ emits: ["update:modelValue"],
+ setup(props, { attrs, emit, slots }) {
+ const optionNodes = () => props.options.map(option => {
+ const value = typeof option === "object" ? option.value : option;
+ const label = typeof option === "object" ? option.label : option;
+ return h("option", { value }, label);
+ });
+
+ const state = normalizeInputState(props.state);
+
+ return () => h("div", { class: cx("form-group", state) }, [
+ props.label && h("label", { class: "label" }, [
+ props.label,
+ iconNode(props.icon),
+ h("div", { class: "select-wrap" }, [
+ h("select", {
+ ...attrs,
+ value: props.modelValue,
+ class: cx("input select", attrs.class),
+ onChange: event => emit("update:modelValue", eventValue(event))
+ }, slots.default?.() || optionNodes())
+ ])
+ ]),
+ !props.label && h("div", { class: "select-wrap" }, [
+ h("select", {
+ ...attrs,
+ value: props.modelValue,
+ class: cx("input select", attrs.class),
+ onChange: event => emit("update:modelValue", eventValue(event))
+ }, slots.default?.() || optionNodes())
+ ]),
+ props.help && h("p", { class: cx("input-help", state && `is-${state}`) }, props.help)
+ ]);
+ }
+});
diff --git a/src/vue/components/GnTextarea.js b/src/vue/components/GnTextarea.js
new file mode 100644
index 0000000..9b9df38
--- /dev/null
+++ b/src/vue/components/GnTextarea.js
@@ -0,0 +1,38 @@
+import { defineComponent, h } from "vue";
+import { cx, eventValue, iconNode, normalizeInputState } from "../utils.js";
+
+export default defineComponent({
+ name: "GnTextarea",
+ inheritAttrs: false,
+ props: {
+ modelValue: { type: String, default: "" },
+ label: { type: String, default: "" },
+ icon: { type: String, default: "" },
+ state: { type: String, default: "" },
+ help: { type: String, default: "" }
+ },
+ emits: ["update:modelValue"],
+ setup(props, { attrs, emit }) {
+ const state = normalizeInputState(props.state);
+
+ return () => h("div", { class: cx("form-group", state) }, [
+ props.label && h("label", { class: "label" }, [
+ props.label,
+ iconNode(props.icon),
+ h("textarea", {
+ ...attrs,
+ value: props.modelValue,
+ class: cx("input", { "input-icon-left": props.icon }, attrs.class),
+ onInput: event => emit("update:modelValue", eventValue(event))
+ })
+ ]),
+ !props.label && h("textarea", {
+ ...attrs,
+ value: props.modelValue,
+ class: cx("input", attrs.class),
+ onInput: event => emit("update:modelValue", eventValue(event))
+ }),
+ props.help && h("p", { class: cx("input-help", state && `is-${state}`) }, props.help)
+ ]);
+ }
+});
diff --git a/src/vue/index.js b/src/vue/index.js
index 3131069..7b96712 100644
--- a/src/vue/index.js
+++ b/src/vue/index.js
@@ -12,4 +12,7 @@
export { default as GnRadio } from "./components/GnRadio.js";
export { default as GnRadioGroup } from "./components/GnRadioGroup.js";
export { default as GnSwitch } from "./components/GnSwitch.js";
+export { default as GnSelect } from "./components/GnSelect.js";
+export { default as GnTextarea } from "./components/GnTextarea.js";
+export { default as GnCard } from "./components/GnCard.js";
export { components, default as VMKUiVue } from "./plugin.js";
diff --git a/src/vue/plugin.js b/src/vue/plugin.js
index 064900d..571f84a 100644
--- a/src/vue/plugin.js
+++ b/src/vue/plugin.js
@@ -12,6 +12,9 @@
import GnRadio from "./components/GnRadio.js";
import GnRadioGroup from "./components/GnRadioGroup.js";
import GnSwitch from "./components/GnSwitch.js";
+import GnSelect from "./components/GnSelect.js";
+import GnTextarea from "./components/GnTextarea.js";
+import GnCard from "./components/GnCard.js";
export const components = {
GnButton,
@@ -22,7 +25,10 @@
GnCheckbox,
GnRadio,
GnRadioGroup,
- GnSwitch
+ GnSwitch,
+ GnSelect,
+ GnTextarea,
+ GnCard
};
export default {