diff --git a/PROJECT_NOTES.md b/PROJECT_NOTES.md index d7bf5c9..9674885 100644 --- a/PROJECT_NOTES.md +++ b/PROJECT_NOTES.md @@ -181,6 +181,13 @@ - `demo/partials/timeline.html` - `demo/partials/accordion.html` +Добавлены Drawer / Side Panel и demo/docs для Confirm Dialog: + +- `src/js/components/drawer.js` +- `src/scss/components/_drawer.scss` +- `demo/partials/drawer.html` +- `demo/partials/confirm-dialog.html` + Публичный JS API: - `GNexusUIKit.Helper` @@ -189,6 +196,7 @@ - `GNexusUIKit.advancedSelect` - `GNexusUIKit.editableString` - `GNexusUIKit.confirmPopup` +- `GNexusUIKit.Drawer` - `GNexusUIKit.Overlays` - `GNexusUIKit.InputPatterns` @@ -198,8 +206,7 @@ Ближайшие полезные additions: -- Drawer / Side Panel: контекстные детали и quick edit без полной модалки. -- Confirm Dialog docs: `confirm-popup.js` уже есть, но его нужно вывести в demo/docs. +- На текущий момент список запланированных компонентов закрыт; дальше расширять набор по новым сценариям. Технический backlog: diff --git a/README.md b/README.md index 9689fd4..d301f2d 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ GNexusUIKit.advancedSelect GNexusUIKit.editableString GNexusUIKit.confirmPopup +GNexusUIKit.Drawer GNexusUIKit.Overlays GNexusUIKit.InputPatterns ``` @@ -139,9 +140,11 @@ - Avatar / Identity - Timeline / Activity Log - Accordion / Disclosure +- Drawer / Side Panel - Toasts - Cards - Modals +- Confirm Dialog Каждая секция demo содержит визуальный пример, короткое описание и копируемый блок кода. @@ -205,6 +208,16 @@ - Accordion: группы на native `
` / ``. - Disclosure: компактная одиночная раскрываемая группа. +### Drawer и Confirm Dialog + +Секции включают: + +- Drawer: боковая overlay-панель для деталей, quick edit и secondary flows. +- Side Panel variants: правая и левая позиция через `position: "left"`. +- Confirm Dialog: документация для существующего `confirmPopup`. + +JS-модули: `src/js/components/drawer.js` и `src/js/components/confirm-popup.js`. + ## SCSS Базовые файлы: @@ -247,8 +260,7 @@ Ближайшие полезные компоненты: -- Drawer / Side Panel: контекстные детали и quick edit без полной модалки. -- Confirm Dialog docs: `confirm-popup.js` уже есть в JS, но его нужно вынести в demo/docs. +- На текущий момент список запланированных компонентов закрыт; дальше можно расширять набор по новым сценариям. Технические задачи: diff --git a/demo/index.html b/demo/index.html index 89d51d2..7db3448 100644 --- a/demo/index.html +++ b/demo/index.html @@ -118,6 +118,11 @@
  • + + Drawer + +
  • +
  • Toasts @@ -132,6 +137,11 @@ Modals
  • +
  • + + Confirm Dialog + +
  • @@ -156,9 +166,11 @@ @@include("partials/avatar.html") @@include("partials/timeline.html") @@include("partials/accordion.html") + @@include("partials/drawer.html") @@include("partials/toasts.html") @@include("partials/cards.html") @@include("partials/modals.html") + @@include("partials/confirm-dialog.html") diff --git a/demo/partials/confirm-dialog.html b/demo/partials/confirm-dialog.html new file mode 100644 index 0000000..11f4668 --- /dev/null +++ b/demo/partials/confirm-dialog.html @@ -0,0 +1,54 @@ +
    +

    Confirm Dialog

    +

    + Confirm Dialog использует существующий `confirmPopup` поверх Modal. Он нужен для коротких подтверждений перед + опасными или необратимыми действиями. +

    + +
    +

    Trigger

    + +
    + +
    +
    + +
    +
    + Confirm JS + +
    +
    confirmPopup(
    +  "This action cannot be undone.",
    +  () => Toasts.createSuccess("Confirmed", "Action accepted").show(),
    +  () => Toasts.createInfo("Canceled", "Action skipped").show()
    +);
    +
    + +
    +
    + Global Namespace + +
    +
    GNexusUIKit.confirmPopup(
    +  "Apply this change?",
    +  () => console.log("confirmed"),
    +  () => console.log("canceled")
    +);
    +
    +
    + + diff --git a/demo/partials/drawer.html b/demo/partials/drawer.html new file mode 100644 index 0000000..4b8fa4d --- /dev/null +++ b/demo/partials/drawer.html @@ -0,0 +1,128 @@ +
    +

    Drawer

    +

    + Drawer подходит для контекстных деталей, quick edit и длинных вторичных сценариев без ухода со страницы. + По API он близок к Modal, но панель закреплена у края экрана. +

    + +
    +

    Examples

    + +
    +
    +

    + Используй Drawer, когда нужно сохранить контекст основного экрана и раскрыть боковую область с формой, + метаданными или журналом. +

    + +
    + + + +
    +
    + + +
    +
    + +
    +
    + Drawer JS + +
    +
    Drawer.create("details-drawer", {
    +  title: "Details",
    +  bodyHtml: `
    +    <p class="text">Context content, forms, logs, metadata.</p>
    +  `,
    +  actions: drawer => {
    +    const close = document.createElement("button");
    +    close.className = "btn btn-primary";
    +    close.textContent = "Close";
    +    close.addEventListener("click", () => drawer.close());
    +    return [close];
    +  }
    +}).show();
    +
    + +
    +
    + Left Position + +
    +
    Drawer.create("navigation-drawer", {
    +  title: "Navigation",
    +  position: "left",
    +  bodyText: "Left drawer content"
    +}).show();
    +
    +
    + + diff --git a/src/js/components/drawer.js b/src/js/components/drawer.js new file mode 100644 index 0000000..7d96cba --- /dev/null +++ b/src/js/components/drawer.js @@ -0,0 +1,135 @@ +function appendContent(container, content, mode = "html") { + if(content instanceof Node) { + container.append(content); + return; + } + + if(typeof content != "undefined" && content !== null) { + if(mode === "text") { + container.textContent = content; + } else { + container.innerHTML = content; + } + } +} + +function template(id, title, footer, props = {}) { + const drawer = document.createElement("div"); + drawer.className = "drawer"; + drawer.setAttribute("aria-hidden", "true"); + drawer.id = id; + + if(props.position === "left") { + drawer.classList.add("drawer-left"); + } + + const backdrop = document.createElement("div"); + backdrop.className = "drawer-backdrop"; + + const panel = document.createElement("aside"); + panel.className = "drawer-panel"; + panel.setAttribute("role", "dialog"); + panel.setAttribute("aria-modal", "true"); + panel.setAttribute("aria-labelledby", `${id}-title`); + + const header = document.createElement("header"); + header.className = "drawer-header"; + + const drawerTitle = document.createElement("h4"); + drawerTitle.className = "drawer-title"; + drawerTitle.id = `${id}-title`; + drawerTitle.textContent = title; + + const close = document.createElement("button"); + close.className = "btn-icon drawer-close"; + close.type = "button"; + close.setAttribute("aria-label", "Close"); + close.textContent = "✕"; + + const body = document.createElement("div"); + body.className = "drawer-body"; + + const drawerFooter = document.createElement("footer"); + drawerFooter.className = "drawer-footer"; + appendContent(drawerFooter, footer, props.footerMode ?? "html"); + + header.append(drawerTitle, close); + panel.append(header, body, drawerFooter); + drawer.append(backdrop, panel); + + return drawer; +} + +function init(drawer, onready) { + drawer.show = function() { + document.querySelector("body").append(drawer); + + setTimeout(() => { + drawer.classList.add("a-show"); + }, 10); + }; + + drawer.close = function() { + drawer.classList.add("a-hide"); + setTimeout(() => { + drawer.remove(); + }, 300); + }; + + drawer.querySelector(".drawer-close").addEventListener("click", () => { + drawer.close(); + }); + + drawer.querySelector(".drawer-backdrop").addEventListener("click", () => { + drawer.close(); + }); + + if(typeof onready == "function") { + onready(drawer); + } + + return drawer; +} + +function create(id, props) { + props = props ?? {}; + const title = props.title || ""; + const footer = props.footer || ""; + + const drawer = template(id, title, footer, props); + + const drawerBody = drawer.querySelector(".drawer-body"); + const drawerFooter = drawer.querySelector(".drawer-footer"); + + if(typeof props.actions == "function") { + const actionsResult = props.actions(drawer); + + if(Array.isArray(actionsResult) && actionsResult[0] instanceof Node) { + const actions = document.createElement("div"); + actions.classList.add("actions"); + for(let actionElement of actionsResult) { + actions.append(actionElement); + } + + drawerFooter.append(actions); + } else if(actionsResult instanceof Node) { + drawerFooter.append(actionsResult); + } + } + + if(typeof props.body == "function") { + const bodyResult = props.body(drawer); + + appendContent(drawerBody, bodyResult, props.bodyMode ?? "html"); + } else if(typeof props.bodyText != "undefined") { + appendContent(drawerBody, props.bodyText, "text"); + } else if(typeof props.bodyHtml != "undefined") { + appendContent(drawerBody, props.bodyHtml, "html"); + } + + return init(drawer, props?.onready); +} + +export default { + create +}; diff --git a/src/js/index.js b/src/js/index.js index d5793b2..325c80d 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -4,6 +4,7 @@ import advancedSelect from "./components/advanced-select.js"; import editableString from "./components/editable-string.js"; import confirmPopup from "./components/confirm-popup.js"; +import Drawer from "./components/drawer.js"; import Overlays from "./components/overlays.js"; import InputPatterns from "./components/input-patterns.js"; import demoNavigation from "./demo-navigation.js"; @@ -16,6 +17,7 @@ advancedSelect, editableString, confirmPopup, + Drawer, Overlays, InputPatterns }; @@ -37,6 +39,7 @@ advancedSelect, editableString, confirmPopup, + Drawer, Overlays, InputPatterns }; diff --git a/src/scss/components/_drawer.scss b/src/scss/components/_drawer.scss new file mode 100644 index 0000000..7915532 --- /dev/null +++ b/src/scss/components/_drawer.scss @@ -0,0 +1,161 @@ +@use "../kit-deps" as *; +@use "typography" as *; + +.drawer { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + justify-content: flex-end; + pointer-events: none; + + .drawer-backdrop { + position: fixed; + inset: 0; + z-index: 1010; + background: $color-black; + opacity: 0; + transition-duration: $motion-slow; + transition-timing-function: $motion-ease; + transition-property: opacity; + pointer-events: auto; + } + + .drawer-panel { + position: relative; + z-index: 1020; + width: min(460px, calc(100vw - #{$space-5})); + min-height: 100vh; + display: flex; + flex-direction: column; + gap: $space-4; + background: $surface-page; + border-left: $border-width-base solid $color-text-light; + box-shadow: -18px 0 42px rgba($color-black, 0.38); + opacity: 0; + transform: translateX(100%); + transition-duration: $motion-slow; + transition-timing-function: $motion-ease; + transition-property: opacity, transform; + pointer-events: auto; + } + + .drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding-right: $space-4; + border-bottom: $border-width-base solid $border-color-muted; + } + + .drawer-title { + margin: 0; + padding: $space-3 $space-4; + background: $color-text-light; + color: $color-black; + text-transform: uppercase; + letter-spacing: $letter-spacing-wide; + } + + .drawer-body { + flex: 1; + overflow-y: auto; + padding: $space-5; + } + + .drawer-footer { + padding: $space-5; + border-top: $border-width-base solid $border-color-muted; + + .actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: $space-3; + width: 100%; + } + } + + &.drawer-left { + justify-content: flex-start; + + .drawer-panel { + border-left: 0; + border-right: $border-width-base solid $color-text-light; + box-shadow: 18px 0 42px rgba($color-black, 0.38); + transform: translateX(-100%); + } + } + + &.a-show { + .drawer-backdrop { + opacity: 0.82; + } + + .drawer-panel { + opacity: 1; + transform: translateX(0); + } + } + + &.a-hide { + .drawer-backdrop { + opacity: 0; + } + + .drawer-panel { + opacity: 0; + transform: translateX(100%); + } + + &.drawer-left .drawer-panel { + transform: translateX(-100%); + } + } +} + +.drawer-preview { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(180px, 280px); + gap: $space-5; + align-items: stretch; + padding: $space-5; + border: $border-width-base solid $border-color-muted; + border-left-width: $border-width-accent; + background: $surface-panel-muted; + + .drawer-preview-content { + display: flex; + flex-direction: column; + gap: $space-3; + } + + .drawer-preview-panel { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-4; + border: $border-width-base solid $color-secondary; + background: $surface-panel; + } + + .drawer-preview-title { + margin: 0; + color: $color-secondary; + font-size: $font-size-md; + text-transform: uppercase; + } + + .drawer-preview-text { + margin: 0; + color: $color-text-medium; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + } +} + +@media (max-width: 720px) { + .drawer-preview { + grid-template-columns: 1fr; + } +} diff --git a/src/scss/kit.scss b/src/scss/kit.scss index 8ca068f..11bffc2 100644 --- a/src/scss/kit.scss +++ b/src/scss/kit.scss @@ -17,6 +17,7 @@ @use "components/stepper"; @use "components/timeline"; @use "components/accordion"; +@use "components/drawer"; @use "components/toasts"; @use "components/cards"; @use "components/modals";