diff --git a/demo/index.html b/demo/index.html index d7c67f7..b442c24 100644 --- a/demo/index.html +++ b/demo/index.html @@ -15,7 +15,13 @@

VMK UI Kit

Demo page placeholder. Component sections will be added here.

+ @@include('./partials/typography.html') + @@include('./partials/palette.html') + @@include('./partials/utilities.html') @@include('./partials/buttons.html') + @@include('./partials/inputs.html') + @@include('./partials/badges.html') + @@include('./partials/alerts.html') diff --git a/demo/partials/alerts.html b/demo/partials/alerts.html new file mode 100644 index 0000000..5ec78cd --- /dev/null +++ b/demo/partials/alerts.html @@ -0,0 +1,50 @@ +
+

Alerts

+ +
+
+ +
Primary alert with default content.
+
+
+ +
+
+ +
+
Success
+ The operation completed successfully. +
+
+
+ +
+
+ +
+
Warning
+ Please review your settings before continuing. +
+
+
+ +
+
+ +
+
Error
+ Something went wrong. Please try again. +
+
+
+ +
+
+ +
+
Info
+ Here is some useful information. +
+
+
+
diff --git a/demo/partials/badges.html b/demo/partials/badges.html new file mode 100644 index 0000000..8aee007 --- /dev/null +++ b/demo/partials/badges.html @@ -0,0 +1,40 @@ +
+

Badges

+ +

Variants

+
+ Primary + Secondary + Accent + Success + Warning + Info + Danger +
+ +

Outline

+
+ Primary outline + Success outline + Danger outline +
+ +

Sizes

+
+ Small + Default + Large +
+ +

With icon

+
+ + + Verified + + + + Failed + +
+
diff --git a/demo/partials/inputs.html b/demo/partials/inputs.html new file mode 100644 index 0000000..08c269d --- /dev/null +++ b/demo/partials/inputs.html @@ -0,0 +1,64 @@ +
+

Inputs

+ +

Sizes

+
+
+ +
+
+ +
+
+ +
+
+ +

With icon

+
+
+ +
+
+ +

States

+
+
+ +

This field has an error.

+
+
+ +

Looks good.

+
+
+ +

Please review.

+
+
+ +

Disabled

+
+
+ +
+
+
diff --git a/demo/partials/palette.html b/demo/partials/palette.html new file mode 100644 index 0000000..43d0a19 --- /dev/null +++ b/demo/partials/palette.html @@ -0,0 +1,49 @@ +
+

Palette

+ +

Neutral

+
+
0 #FCFCFC
+
50 #F7F7F8
+
100 #F0F0F0
+
200 #E1E2E3
+
300 #D9D9D9
+
400 #B7BABE
+
500 #9CA3AF
+
600 #82868D
+
700 #6B7280
+
800 #3F4145
+
900 #1A1A1A
+
+ +

Mint

+
+
50 #F2FAFB
+
200 #DFF1F4
+
300 #D9EEF2
+
600 #7EC0C9
+
800 #316873
+
900 #1F4B52
+
+ +

Yellow

+
+
50 #FEFEF0
+
100 #FDFCB8
+
300 #FCFD76
+
400 #F0F166
+
500 #E8E94A
+
+ +

Semantic

+
+
Success bg #DDF3E4
+
Success text #1E7A46
+
Warning bg #FEF0DD
+
Warning text #B45309
+
Info bg #EAF4FE
+
Info text #2563A6
+
Danger bg #FBE4E2
+
Danger text #D64545
+
+
diff --git a/demo/partials/typography.html b/demo/partials/typography.html new file mode 100644 index 0000000..c73c247 --- /dev/null +++ b/demo/partials/typography.html @@ -0,0 +1,23 @@ +
+

Typography

+

Base font: Plus Jakarta Sans (400/500/600/700).

+ +

Headings

+

Heading 1 / 40px bold

+

Heading 2 / 32px bold

+

Heading 3 / 28px semibold

+

Heading 4 / 22px semibold

+
Heading 5 / 18px semibold
+
Heading 6 / 16px semibold
+ +

Body

+

Body large — 18/28 regular

+

Body medium — 16/24 regular

+

Body small — 14/20 regular

+

Body xsmall — 12/16 regular

+ +

Special

+

Button text — 16/22 semibold

+

Caption — 12/16 regular

+

Overline — 12/14 medium

+
diff --git a/demo/partials/utilities.html b/demo/partials/utilities.html new file mode 100644 index 0000000..9f97052 --- /dev/null +++ b/demo/partials/utilities.html @@ -0,0 +1,36 @@ +
+

Utilities

+ +

Spacing scale

+
+
0
+
4px
+
8px
+
12px
+
16px
+
20px
+
24px
+
32px
+
40px
+
48px
+
+ +

Display helpers

+
+ d-inline + d-inline-block + d-block + d-none + d-flex + d-grid +
+ +

Radius tokens

+
+
+
+
+
+
+
+
diff --git a/docs/component-coverage.md b/docs/component-coverage.md index ad1a61f..c6c10f7 100644 --- a/docs/component-coverage.md +++ b/docs/component-coverage.md @@ -6,17 +6,35 @@ | Компонент | Статус | Примечание | |-----------|--------|------------| -| Typography | — | Ждёт токены из Figma | -| Palette | — | Ждёт токены из Figma | -| Spacing | — | Ждёт токены из Figma | +| Typography | done | Demo partial + базовые HTML-теги | +| Palette | done | Demo partial со swatches из Figma | +| Spacing | done | `$space-0..12` + utility классы `.m-*`/`.p-*`/`.g-*` | ## Компоненты | Компонент | Статус | Vue adapter | Demo | |-----------|--------|-------------|------| -| Button | — | — | — | -| Input | — | — | — | -| Badge | — | — | — | -| Alert | — | — | — | +| Button | done | GnButton | partial | +| Input | done | GnInput | partial | +| Badge | done | GnBadge | partial | +| Alert | done | GnAlert | partial | +| Checkbox | — | — | — | +| Radio | — | — | — | +| Select | — | — | — | +| Switch | — | — | — | +| Card | — | — | — | +| Modal | — | — | — | +| Drawer | — | — | — | +| Toast | — | — | — | +| Tabs | — | — | — | +| Table | — | — | — | +| List | — | — | — | +| Avatar | — | — | — | +| Chip | — | — | — | +| Progress | — | — | — | +| Skeleton | — | — | — | +| Tooltip | — | — | — | +| Dropdown | — | — | — | +| NavigationShell | — | — | — | Статусы: `—` не начато, `WIP` в работе, `done` готово, `redesigned` перерисован в новом стиле. diff --git a/examples/vue/src/main.js b/examples/vue/src/main.js index a5ffd93..fb651d6 100644 --- a/examples/vue/src/main.js +++ b/examples/vue/src/main.js @@ -18,6 +18,17 @@ Toggle loading Disabled +
+ + +
+
+ Verified + Draft +
+
+ Vue adapter alert component. +
` }; diff --git a/src/scss/components/_alerts.scss b/src/scss/components/_alerts.scss new file mode 100644 index 0000000..da8c89b --- /dev/null +++ b/src/scss/components/_alerts.scss @@ -0,0 +1,74 @@ +/* ========================= + Alerts + Figma: Alerts / Banner. +========================= */ + +@use "../kit-deps" as *; + +.alert { + display: flex; + align-items: flex-start; + gap: $space-3; + width: 100%; + max-width: 760px; + padding: $space-3 $space-4; + border: $border-width-base $border-style-base transparent; + border-radius: $radius-md; + background-color: $neutral-100; + color: $neutral-900; + font-family: $font-family-base; + font-size: 14px; + line-height: 1.4; + + .vmk-icon { + flex-shrink: 0; + font-size: $icon-size-lg; + color: currentColor; + } + + .alert-content { + flex: 1; + } + + .alert-title { + font-weight: $font-weight-semibold; + margin-bottom: $space-1; + } + + // Variants + &.alert-primary { + background-color: $yellow-100; + color: $neutral-900; + } + + &.alert-secondary { + background-color: $mint-50; + color: $mint-900; + } + + &.alert-accent { + background-color: $yellow-100; + color: $neutral-900; + } + + &.alert-success { + background-color: $success-bg; + color: $success-text; + } + + &.alert-warning { + background-color: $warning-bg; + color: $warning-text; + } + + &.alert-info { + background-color: $info-bg; + color: $info-text; + } + + &.alert-danger, + &.alert-error { + background-color: $danger-bg; + color: $danger-text; + } +} diff --git a/src/scss/components/_badges.scss b/src/scss/components/_badges.scss new file mode 100644 index 0000000..e3e3e7e --- /dev/null +++ b/src/scss/components/_badges.scss @@ -0,0 +1,98 @@ +/* ========================= + Badges + Figma: Badges. +========================= */ + +@use "../kit-deps" as *; + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: $space-1; + min-height: 24px; + padding: $space-1 $space-2; + font-family: $font-family-base; + font-size: 12px; + font-weight: $font-weight-semibold; + line-height: 1; + text-transform: none; + white-space: nowrap; + border-radius: $radius-full; + background-color: $neutral-200; + color: $neutral-900; + transition: + background-color $motion-base $motion-ease, + color $motion-base $motion-ease; + + &.with-icon { + padding-left: $space-1; + } + + .vmk-icon { + font-size: 14px; + } + + // Variants + &.badge-primary { + background-color: $yellow-300; + color: $neutral-900; + } + + &.badge-secondary { + background-color: $mint-300; + color: $mint-900; + } + + &.badge-accent { + background-color: $yellow-300; + color: $neutral-900; + } + + &.badge-success { + background-color: $success-bg; + color: $success-text; + } + + &.badge-warning { + background-color: $warning-bg; + color: $warning-text; + } + + &.badge-info { + background-color: $info-bg; + color: $info-text; + } + + &.badge-danger, + &.badge-error { + background-color: $danger-bg; + color: $danger-text; + } + + // Outline + &.badge-outline { + background-color: transparent; + border: $border-width-base $border-style-base $neutral-900; + color: $neutral-900; + } + + &.badge-primary-outline { + background-color: transparent; + border: $border-width-base $border-style-base $neutral-900; + color: $neutral-900; + } + + // Sizes + &.badge-sm { + min-height: 20px; + padding: 2px $space-2; + font-size: 11px; + } + + &.badge-lg { + min-height: 28px; + padding: $space-1 $space-3; + font-size: 13px; + } +} diff --git a/src/scss/components/_forms.scss b/src/scss/components/_forms.scss new file mode 100644 index 0000000..5950600 --- /dev/null +++ b/src/scss/components/_forms.scss @@ -0,0 +1,151 @@ +/* ========================= + Forms / Input + Figma: Inputs / Text Field. +========================= */ + +@use "sass:color"; +@use "../kit-deps" as *; + +.form-group { + display: flex; + flex-direction: column; + gap: $space-1; + width: 100%; + max-width: 600px; + + .label { + display: flex; + flex-direction: column; + gap: $space-1; + font-size: 14px; + font-weight: $font-weight-semibold; + color: $neutral-900; + position: relative; + + .vmk-icon { + position: absolute; + left: $space-3; + bottom: $space-3; + color: $neutral-700; + font-size: $icon-size-md; + pointer-events: none; + transition: color $motion-base $motion-ease; + } + + &.is-required::after { + content: "*"; + color: $danger-text; + margin-left: $space-1; + } + } + + .input { + min-height: 48px; + padding: $space-3 $space-4; + font-family: $font-family-base; + font-size: 15px; + font-weight: $font-weight-medium; + line-height: 1.25; + color: $neutral-900; + background-color: $neutral-0; + border: $border-width-base $border-style-base $neutral-400; + border-radius: $radius-md; + transition: + border-color $motion-base $motion-ease, + box-shadow $motion-base $motion-ease, + background-color $motion-base $motion-ease; + + &:hover:not(:disabled):not([readonly]) { + border-color: $mint-600; + } + + &:focus { + outline: none; + border-color: $mint-600; + box-shadow: 0 0 0 3px #{rgba($mint-600, 0.24)}; + background-color: $neutral-0; + } + + &::placeholder { + color: $neutral-600; + font-weight: $font-weight-regular; + } + + &:disabled { + color: $neutral-500; + background-color: $neutral-50; + border-color: $neutral-300; + cursor: not-allowed; + opacity: 0.72; + } + + &[readonly] { + color: $neutral-700; + background-color: $neutral-50; + border-color: $neutral-300; + } + + &.input-sm { + min-height: 40px; + padding: $space-2 $space-3; + font-size: 14px; + border-radius: $radius-sm; + } + + &.input-lg { + min-height: 56px; + padding: $space-3 $space-5; + font-size: 16px; + border-radius: $radius-lg; + } + + &.input-icon-left { + padding-left: $space-8; + } + + &.input-icon-right { + padding-right: $space-8; + } + } + + .input-help { + font-size: 13px; + color: $neutral-700; + margin: 0; + + &.is-error { color: $danger-text; } + &.is-success { color: $success-text; } + &.is-warning { color: $warning-text; } + } + + &.is-error .input:not(:focus) { + border-color: $danger-text; + background-color: $danger-bg; + + &:hover { + border-color: color.adjust($danger-text, $lightness: -8%); + } + } + + &.is-success .input:not(:focus) { + border-color: $success-text; + background-color: $success-bg; + } + + &.is-warning .input:not(:focus) { + border-color: $warning-text; + background-color: $warning-bg; + } +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: $space-4; + width: 100%; + max-width: 760px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +} diff --git a/src/scss/demo.scss b/src/scss/demo.scss index d5b5f34..572760c 100644 --- a/src/scss/demo.scss +++ b/src/scss/demo.scss @@ -36,3 +36,49 @@ gap: $space-3; margin-bottom: $space-3; } + +.swatch { + display: flex; + flex-direction: column; + gap: $space-1; + font-size: 11px; + color: $neutral-700; + + .swatch-color { + width: 64px; + height: 64px; + border-radius: $radius-md; + box-shadow: inset 0 0 0 1px rgba($neutral-900, 0.08); + } +} + +.spacing-block { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + background-color: $mint-200; + border: 1px solid $mint-400; + border-radius: $radius-sm; + font-size: 11px; + color: $mint-900; +} + +.utility-box { + padding: $space-1 $space-2; + background-color: $neutral-100; + border-radius: $radius-sm; + font-size: 12px; +} + +.radius-box { + width: 48px; + height: 48px; + background-color: $mint-300; + + &.radius-sm { border-radius: $radius-sm; } + &.radius-md { border-radius: $radius-md; } + &.radius-lg { border-radius: $radius-lg; } + &.radius-xl { border-radius: $radius-xl; } + &.radius-full { border-radius: $radius-full; } +} diff --git a/src/scss/kit.scss b/src/scss/kit.scss index 92d86d1..453f00d 100644 --- a/src/scss/kit.scss +++ b/src/scss/kit.scss @@ -3,6 +3,9 @@ @use "motion"; @use "components/icons"; @use "components/buttons"; +@use "components/badges"; +@use "components/alerts"; +@use "components/forms"; @use "utils"; * { diff --git a/src/vue/components/GnAlert.js b/src/vue/components/GnAlert.js new file mode 100644 index 0000000..24b155d --- /dev/null +++ b/src/vue/components/GnAlert.js @@ -0,0 +1,39 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode, normalizeVariant } from "../utils.js"; + +const STATE_ICONS = { + success: "essentials/check/circle", + warning: "essentials/alert-sign", + info: "essentials/information/circle", + danger: "essentials/alert/circle", + error: "essentials/alert/circle" +}; + +export default defineComponent({ + name: "GnAlert", + inheritAttrs: false, + props: { + variant: { type: String, default: "primary" }, + icon: { type: String, default: "" }, + role: { type: String, default: "status" }, + title: { type: String, default: "" } + }, + setup(props, { attrs, slots }) { + return () => { + const variant = normalizeVariant(props.variant); + const iconName = props.icon || STATE_ICONS[variant] || ""; + + return h("div", { + ...attrs, + role: props.role, + class: cx("alert", `alert-${variant}`, attrs.class) + }, [ + iconNode(iconName), + h("div", { class: "alert-content" }, [ + props.title && h("div", { class: "alert-title" }, props.title), + slots.default?.() + ]) + ]); + }; + } +}); diff --git a/src/vue/components/GnBadge.js b/src/vue/components/GnBadge.js new file mode 100644 index 0000000..c4ef91a --- /dev/null +++ b/src/vue/components/GnBadge.js @@ -0,0 +1,45 @@ +import { defineComponent, h } from "vue"; +import { cx, iconNode, normalizeVariant } from "../utils.js"; + +export default defineComponent({ + name: "GnBadge", + inheritAttrs: false, + props: { + variant: { type: String, default: "primary" }, + outline: { type: Boolean, default: false }, + icon: { type: String, default: "" }, + size: { type: String, default: "" } + }, + setup(props, { attrs, slots }) { + return () => { + const variant = normalizeVariant(props.variant); + const hasIcon = Boolean(props.icon); + + let variantClass; + if (props.outline && variant === "primary") { + variantClass = "badge-primary-outline"; + } else if (props.outline) { + variantClass = `badge-${variant} badge-outline`; + } else { + variantClass = `badge-${variant}`; + } + + return h("span", { + ...attrs, + class: cx( + "badge", + variantClass, + { + "with-icon": hasIcon, + "badge-sm": props.size === "sm", + "badge-lg": props.size === "lg" + }, + attrs.class + ) + }, [ + iconNode(props.icon), + slots.default?.() + ]); + }; + } +}); diff --git a/src/vue/components/GnInput.js b/src/vue/components/GnInput.js new file mode 100644 index 0000000..32bea11 --- /dev/null +++ b/src/vue/components/GnInput.js @@ -0,0 +1,66 @@ +/** + * GnInput - Text input field with label, icon, state, and help text. + * + * @typedef {Object} GnInputProps + * @property {string|number} [modelValue=''] - Bound value + * @property {string} [label=''] - Label text + * @property {string} [type='text'] - input type attribute + * @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 {boolean} [bare=false] - Render bare input without .form-group wrapper + * + * @emits update:modelValue + */ + +import { defineComponent, h } from "vue"; +import { cx, eventValue, iconNode, normalizeInputState } from "../utils.js"; + +export default defineComponent({ + name: "GnInput", + inheritAttrs: false, + props: { + modelValue: { type: [String, Number], default: "" }, + label: { type: String, default: "" }, + type: { type: String, default: "text" }, + icon: { type: String, default: "" }, + state: { type: String, default: "" }, + help: { type: String, default: "" }, + bare: { type: Boolean, default: false } + }, + emits: ["update:modelValue"], + setup(props, { attrs, emit }) { + return () => { + const state = normalizeInputState(props.state); + const hasIcon = Boolean(props.icon); + + const input = h("input", { + ...attrs, + type: props.type, + value: props.modelValue, + class: cx( + props.bare ? "" : "input", + { + "input-icon-left": hasIcon + }, + attrs.class + ), + onInput: event => emit("update:modelValue", eventValue(event)) + }); + + if (props.bare) { + return input; + } + + return h("div", { class: cx("form-group", state) }, [ + props.label && h("label", { class: "label" }, [ + props.label, + iconNode(props.icon), + input + ]), + !props.label && input, + 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 e5d49ed..b96cf61 100644 --- a/src/vue/index.js +++ b/src/vue/index.js @@ -5,4 +5,7 @@ export { default as GnButton } from "./components/GnButton.js"; export { default as VmkIcon } from "./components/Icon.js"; +export { default as GnInput } from "./components/GnInput.js"; +export { default as GnBadge } from "./components/GnBadge.js"; +export { default as GnAlert } from "./components/GnAlert.js"; export { components, default as VMKUiVue } from "./plugin.js"; diff --git a/src/vue/plugin.js b/src/vue/plugin.js index 36547ee..c2fc8fc 100644 --- a/src/vue/plugin.js +++ b/src/vue/plugin.js @@ -5,10 +5,16 @@ import GnButton from "./components/GnButton.js"; import VmkIcon from "./components/Icon.js"; +import GnInput from "./components/GnInput.js"; +import GnBadge from "./components/GnBadge.js"; +import GnAlert from "./components/GnAlert.js"; export const components = { GnButton, - VmkIcon + VmkIcon, + GnInput, + GnBadge, + GnAlert }; export default { diff --git a/src/vue/utils.js b/src/vue/utils.js index 6a56cc0..d999a42 100644 --- a/src/vue/utils.js +++ b/src/vue/utils.js @@ -51,6 +51,20 @@ return SIZE_MAP[value] || fallback; } +export const inputVariants = new Set(["error", "success", "warning"]); + +export function normalizeInputState(value, fallback = "") { + return inputVariants.has(value) ? value : fallback; +} + +export function eventValue(event) { + const target = event.target; + if (target.type === "checkbox") { + return target.checked; + } + return target.value; +} + const PHOSPHOR_TO_VMK = { spinner: "essentials/loading", loader: "essentials/loading",