From 17a18c1871511e1862376cc1ade1758ddac39540 Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Mon, 16 Jun 2025 17:58:01 +0800 Subject: [PATCH] Getting ready to clean up the package and release 0.2.0 --- docs/src/_includes/layouts/base.njk | 21 +- docs/src/_includes/macros/code_preview.njk | 3 +- .../partials/kitchen-sink/combobox.njk | 4 +- .../partials/kitchen-sink/dialog.njk | 19 +- .../partials/kitchen-sink/dropdown-menu.njk | 122 +++++----- .../partials/kitchen-sink/select.njk | 2 +- docs/src/_includes/partials/sidebar.njk | 2 +- docs/src/assets/styles.css | 225 ++++++++++++------ docs/src/components/breadcrumb.njk | 3 +- docs/src/components/combobox.njk | 22 +- docs/src/components/dialog.njk | 14 +- docs/src/components/dropdown-menu.njk | 133 +++++------ docs/src/components/popover.njk | 10 +- docs/src/components/select.njk | 18 +- docs/src/components/sidebar.njk | 4 +- docs/src/components/tabs.njk | 10 +- docs/src/components/toast.njk | 4 +- docs/src/index.njk | 4 +- docs/src/installation.njk | 46 +--- docs/src/test.njk | 71 ++++++ package-lock.json | 99 -------- package.json | 1 - scripts/build.js | 16 +- src/css/basecoat.css | 95 ++++---- src/jinja/select.html.jinja | 111 +++++---- src/js/dropdown-menu.js | 105 ++++---- src/js/popover.js | 69 ++++++ src/js/select.js | 108 +++++---- src/nunjucks/dialog.njk | 8 +- src/nunjucks/dropdown-menu.njk | 30 ++- src/nunjucks/popover.njk | 46 ++-- src/nunjucks/select.njk | 60 +++-- src/nunjucks/toast.njk | 4 +- wrangler.jsonc | 7 + 34 files changed, 789 insertions(+), 707 deletions(-) create mode 100644 docs/src/test.njk create mode 100644 src/js/popover.js create mode 100644 wrangler.jsonc diff --git a/docs/src/_includes/layouts/base.njk b/docs/src/_includes/layouts/base.njk index d7a8319..8a95ce2 100644 --- a/docs/src/_includes/layouts/base.njk +++ b/docs/src/_includes/layouts/base.njk @@ -59,6 +59,7 @@ {# JS #} + @@ -71,17 +72,17 @@ {% if head %}{{ head | safe }}{% endif %} diff --git a/docs/src/_includes/macros/code_preview.njk b/docs/src/_includes/macros/code_preview.njk index aaf33d9..9264eed 100644 --- a/docs/src/_includes/macros/code_preview.njk +++ b/docs/src/_includes/macros/code_preview.njk @@ -20,8 +20,7 @@ {{ tabs( tabsets=tabsets, - default_tab_index=(2 if default_code else 1), - register_on=["alpine:init", "htmx:afterSwap"] + default_tab_index=(2 if default_code else 1) ) }} {% endmacro %} \ No newline at end of file diff --git a/docs/src/_includes/partials/kitchen-sink/combobox.njk b/docs/src/_includes/partials/kitchen-sink/combobox.njk index cbfbc87..3713813 100644 --- a/docs/src/_includes/partials/kitchen-sink/combobox.njk +++ b/docs/src/_includes/partials/kitchen-sink/combobox.njk @@ -11,7 +11,7 @@
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %} {% call select( - content_attrs={"class": "w-48"}, + popover_attrs={"class": "w-48"}, listbox_attrs={"data-empty": "No framework found."}, is_combobox=true ) %} @@ -56,7 +56,7 @@ } ] %} {% call select( - content_attrs={"class": "w-72"}, + popover_attrs={"class": "w-72"}, listbox_attrs={"data-empty": "No timezone found."}, is_combobox=true ) %} diff --git a/docs/src/_includes/partials/kitchen-sink/dialog.njk b/docs/src/_includes/partials/kitchen-sink/dialog.njk index 13bf547..81579fd 100644 --- a/docs/src/_includes/partials/kitchen-sink/dialog.njk +++ b/docs/src/_includes/partials/kitchen-sink/dialog.njk @@ -17,10 +17,9 @@ id="demo-dialog-edit-profile", title="Edit profile", description="Make changes to your profile here. Click save when you're done.", - main_attrs={"x-ref": "myDialog"}, trigger="Edit Profile", trigger_attrs={"class": "btn-outline"}, - content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"}, + dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"}, footer=footer ) %}
@@ -36,17 +35,16 @@ {% endcall %} {% set footer %} - + {% endset %} {% call dialog( id="dialog-example", title="Scrollable Content", description="This is a dialog with scrollable content.", - main_attrs={"x-ref": "myDialog"}, trigger="Scrollable Content", trigger_attrs={"class": "btn-outline"}, - content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"}, - content_body_attrs={"class": "overflow-y-auto scrollbar"}, + dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"}, + body_attrs={"class": "overflow-y-auto scrollbar"}, footer=footer ) %}
@@ -62,15 +60,6 @@

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

{% endcall %} - -
\ No newline at end of file diff --git a/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk b/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk index a418341..ff79594 100644 --- a/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk +++ b/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk @@ -13,68 +13,66 @@ id="dropdown-menu-default", trigger="Open", trigger_attrs={"class": "btn-outline"}, - content_attrs={"class": "min-w-56"} + popover_attrs={"class": "min-w-56"} ) %}
- My Account - -
+
Billing ⌘B - -
+
Settings ⌘S - -
+
Keyboard shortcuts ⌘K - +

- - - +
GitHub
+
Support
+
API

- + {% endcall %} {% call dropdown_menu( id="dropdown-menu-checkboxes", trigger="Checkboxes", trigger_attrs={"class": "btn-outline"}, - content_attrs={"class": "min-w-56"}, - register_on=["alpine:init", "htmx:afterSwap"] + popover_attrs={"class": "min-w-56"} ) %}
- Account Options - -
+
{% lucide "credit-card" %} Billing ⌘B - -
+
{% lucide "settings" %} Settings ⌘S - +

- Appearance -
+
⇧⌘P - - - +

- + {% endcall %} {% set profile_menu %} @@ -143,41 +139,39 @@
- +
- - - +
- + {% endset %} {% call dropdown_menu( trigger="Radio Group", trigger_attrs={"class": "btn-outline"}, - content_attrs={"class": "min-w-56"}, - register_on=["alpine:init", "htmx:afterSwap"] + popover_attrs={"class": "min-w-56"} ) %}
Panel Position
- - - +
{% endcall %} @@ -256,7 +248,7 @@ id="dropdown-checkboxes", trigger=trigger, trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"}, - content_attrs={"data-align": "end"} + popover_attrs={"data-align": "end"} ) %} {{ profile_menu | safe }} {% endcall %} @@ -268,21 +260,21 @@ id="dropdown-checkboxes", trigger=trigger, trigger_attrs={"class": "btn-icon-ghost"}, - content_attrs={"class": "min-w-32"} + popover_attrs={"class": "min-w-32"} ) %} - - +
- + {% endcall %} diff --git a/docs/src/_includes/partials/kitchen-sink/select.njk b/docs/src/_includes/partials/kitchen-sink/select.njk index 9bbdd90..937d0aa 100644 --- a/docs/src/_includes/partials/kitchen-sink/select.njk +++ b/docs/src/_includes/partials/kitchen-sink/select.njk @@ -38,7 +38,7 @@ items: [ { type: "item", label: "Apple", value: "apple" }, { type: "item", label: "Banana", value: "banana" }, - { type: "item", label: "Blueberry Blueberry Blueberry Blueberry Blueberry", value: "blueberry" } + { type: "item", label: "Blueberry", value: "blueberry" } ] }, { diff --git a/docs/src/_includes/partials/sidebar.njk b/docs/src/_includes/partials/sidebar.njk index cba5ecb..ecbd276 100644 --- a/docs/src/_includes/partials/sidebar.njk +++ b/docs/src/_includes/partials/sidebar.njk @@ -29,7 +29,7 @@ " } ) %} -
+

I hope you like Basecoat...

My name is Ronan and I made this (and other things). If you find it useful, please consider sponsoring me on GitHub or following me on X.

diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index 567bb7c..616d9e9 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -5,6 +5,7 @@ :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --color-amber-50: oklch(98.7% 0.022 95.277); @@ -63,6 +64,13 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --animate-spin: spin 1s linear infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; @@ -245,6 +253,9 @@ } } @layer utilities { + .\@container { + container-type: inline-size; + } .pointer-events-none { pointer-events: none; } @@ -280,6 +291,9 @@ .top-0 { top: calc(var(--spacing) * 0); } + .top-1 { + top: calc(var(--spacing) * 1); + } .top-1\/2 { top: calc(1/2 * 100%); } @@ -289,9 +303,15 @@ .top-22 { top: calc(var(--spacing) * 22); } + .right-2 { + right: calc(var(--spacing) * 2); + } .right-2\.5 { right: calc(var(--spacing) * 2.5); } + .left-1 { + left: calc(var(--spacing) * 1); + } .left-1\/2 { left: calc(1/2 * 100%); } @@ -355,6 +375,9 @@ .mb-12 { margin-bottom: calc(var(--spacing) * 12); } + .-ml-1 { + margin-left: calc(var(--spacing) * -1); + } .-ml-1\.5 { margin-left: calc(var(--spacing) * -1.5); } @@ -376,9 +399,6 @@ .hidden { display: none; } - .inline { - display: inline; - } .inline-block { display: inline-block; } @@ -402,6 +422,10 @@ width: calc(var(--spacing) * 2.5); height: calc(var(--spacing) * 2.5); } + .size-3 { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } .size-3\.5 { width: calc(var(--spacing) * 3.5); height: calc(var(--spacing) * 3.5); @@ -466,9 +490,15 @@ .min-h-\[350px\] { min-height: 350px; } + .w-1 { + width: calc(var(--spacing) * 1); + } .w-1\/2 { width: calc(1/2 * 100%); } + .w-2 { + width: calc(var(--spacing) * 2); + } .w-2\/3 { width: calc(2/3 * 100%); } @@ -550,21 +580,42 @@ .flex-1 { flex: 1; } + .flex-shrink { + flex-shrink: 1; + } .shrink-0 { flex-shrink: 0; } + .border-collapse { + border-collapse: collapse; + } + .-translate-x-1 { + --tw-translate-x: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } + .translate-y-0 { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .translate-y-0\.5 { --tw-translate-y: calc(var(--spacing) * 0.5); translate: var(--tw-translate-x) var(--tw-translate-y); } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } .animate-pulse { animation: var(--animate-pulse); } @@ -574,6 +625,9 @@ .cursor-pointer { cursor: pointer; } + .resize { + resize: both; + } .scroll-m-20 { scroll-margin: calc(var(--spacing) * 20); } @@ -619,6 +673,9 @@ .justify-start { justify-content: flex-start; } + .gap-0 { + gap: calc(var(--spacing) * 0); + } .gap-0\.5 { gap: calc(var(--spacing) * 0.5); } @@ -674,6 +731,9 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } + .gap-x-1 { + column-gap: calc(var(--spacing) * 1); + } .gap-x-1\.5 { column-gap: calc(var(--spacing) * 1.5); } @@ -763,6 +823,9 @@ .border-amber-50 { border-color: var(--color-amber-50); } + .border-border { + border-color: var(--color-border); + } .border-border\/50 { border-color: var(--color-border); @supports (color: color-mix(in lab, red, red)) { @@ -799,6 +862,9 @@ .bg-sidebar-primary { background-color: var(--color-sidebar-primary); } + .mask-repeat { + mask-repeat: repeat; + } .object-cover { object-fit: cover; } @@ -841,6 +907,9 @@ .px-\[0\.3rem\] { padding-inline: 0.3rem; } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } @@ -2534,7 +2603,10 @@ } @layer components { .dropdown-menu { - [popover].popover { + position: relative; + display: inline-flex; + padding: calc(var(--spacing) * 1); + [data-popover] { padding: calc(var(--spacing) * 1); min-width: anchor-size(width); [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'] { @@ -2606,7 +2678,7 @@ color: var(--color-accent-foreground); } } - [role='menu'] [role='heading'] { + [role='menu'] [role="presentation"] { display: flex; padding-inline: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 1.5); @@ -2762,15 +2834,15 @@ } } @layer components { - [popover].popover { + [data-popover] { visibility: visible; position: absolute; - inset: auto; z-index: 50; width: max-content; - --tw-scale-x: 95%; - --tw-scale-y: 95%; - --tw-scale-z: 95%; + min-width: 100%; + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; scale: var(--tw-scale-x) var(--tw-scale-y); overflow-x: hidden; overflow-y: auto; @@ -2778,106 +2850,85 @@ border-style: var(--tw-border-style); border-width: 1px; background-color: var(--color-popover); - padding: calc(var(--spacing) * 4); color: var(--color-popover-foreground); - opacity: 0%; + opacity: 100%; --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); transition-property: all; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); - transition-behavior: allow-discrete; - &:not([data-side]), &[data-side='bottom'] { - --tw-translate-y: calc(var(--spacing) * -2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='top'] { - --tw-translate-y: calc(var(--spacing) * 2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='left'] { - --tw-translate-x: calc(var(--spacing) * 2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='right'] { - --tw-translate-x: calc(var(--spacing) * -2); - translate: var(--tw-translate-x) var(--tw-translate-y); + &[aria-hidden='true'] { + visibility: hidden; + --tw-scale-x: 95%; + --tw-scale-y: 95%; + --tw-scale-z: 95%; + scale: var(--tw-scale-x) var(--tw-scale-y); + opacity: 0%; + &:not([data-side]), &[data-side='bottom'] { + --tw-translate-y: calc(var(--spacing) * -2); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + &[data-side='top'] { + --tw-translate-y: calc(var(--spacing) * 2); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + &[data-side='left'] { + --tw-translate-x: calc(var(--spacing) * 2); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + &[data-side='right'] { + --tw-translate-x: calc(var(--spacing) * -2); + translate: var(--tw-translate-x) var(--tw-translate-y); + } } &:not([data-side]), &[data-side='bottom'] { - top: anchor(bottom); + top: 100%; margin-top: calc(var(--spacing) * 1); } &[data-side='top'] { - bottom: anchor(top); + bottom: 100%; margin-bottom: calc(var(--spacing) * 1); } &[data-side='left'] { - right: anchor(left); + right: 100%; margin-right: calc(var(--spacing) * 1); } &[data-side='right'] { - left: anchor(right); + left: 100%; margin-left: calc(var(--spacing) * 1); } &:not([data-side]), &[data-side='bottom'], &[data-side='top'] { &:not([data-align]), &[data-align='start'] { - left: anchor(left); + left: calc(var(--spacing) * 0); } &[data-align='end'] { - right: anchor(right); + right: calc(var(--spacing) * 0); } &[data-align='center'] { - left: anchor(center); + left: calc(1/2 * 100%); --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } } &[data-side='left'], &[data-side='right'] { &:not([data-align]), &[data-align='start'] { - top: anchor(top); + top: calc(var(--spacing) * 0); } &[data-align='end'] { - bottom: anchor(bottom); + bottom: calc(var(--spacing) * 0); } &[data-align='center'] { - top: anchor(center); + top: calc(1/2 * 100%); --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } } - &:is(:popover-open) { - --tw-translate-x: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - --tw-translate-y: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - --tw-scale-x: 100%; - --tw-scale-y: 100%; - --tw-scale-z: 100%; - scale: var(--tw-scale-x) var(--tw-scale-y); - opacity: 100%; - @starting-style { - --tw-scale-x: 95%; - --tw-scale-y: 95%; - --tw-scale-z: 95%; - scale: var(--tw-scale-x) var(--tw-scale-y); - opacity: 0%; - &:not([data-side]), &[data-side='bottom'] { - --tw-translate-y: calc(var(--spacing) * -2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='top'] { - --tw-translate-y: calc(var(--spacing) * 2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='left'] { - --tw-translate-x: calc(var(--spacing) * 2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - &[data-side='right'] { - --tw-translate-x: calc(var(--spacing) * -2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } + } + .popover { + position: relative; + display: inline-flex; + [data-popover] { + padding: calc(var(--spacing) * 4); } } } @@ -3159,8 +3210,9 @@ } } *:not(select).select { - [popover].popover { - min-width: anchor-size(width); + position: relative; + display: inline-flex; + [data-popover] { padding: calc(var(--spacing) * 1); [role='option'] { position: relative; @@ -3218,7 +3270,7 @@ background-repeat: no-repeat; } } - [role='listbox'] [role='heading'] { + [role='listbox'] [role="presentation"] { display: flex; padding-inline: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 1.5); @@ -4886,6 +4938,26 @@ code.hljs { inherits: false; initial-value: 0; } +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} @property --tw-space-y-reverse { syntax: "*"; inherits: false; @@ -5100,6 +5172,11 @@ code.hljs { --tw-translate-x: 0; --tw-translate-y: 0; --tw-translate-z: 0; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; --tw-space-y-reverse: 0; --tw-space-x-reverse: 0; --tw-border-style: solid; diff --git a/docs/src/components/breadcrumb.njk b/docs/src/components/breadcrumb.njk index b77403c..89aa8de 100644 --- a/docs/src/components/breadcrumb.njk +++ b/docs/src/components/breadcrumb.njk @@ -29,8 +29,7 @@ toc: {% call dropdown_menu( id="demo-breadcrumb-menu", trigger=trigger, - trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"}, - register_on=["alpine:init", "htmx:afterSwap"] + trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"} ) %}

- +
{% endcall %}{% endset %} {{ code_preview("dropdown-menu-checkboxes", code | prettyHtml) }} @@ -330,8 +320,7 @@ toc:
Panel Position
- - - +
{% endcall %}{% endset %} {{ code_preview("dropdown-menu", code | prettyHtml) }} \ No newline at end of file diff --git a/docs/src/components/popover.njk b/docs/src/components/popover.njk index e5f16af..cfcbbf9 100644 --- a/docs/src/components/popover.njk +++ b/docs/src/components/popover.njk @@ -63,17 +63,13 @@ toc:

HTML + JavaScript

-

Step 1 (Optional): Include the JavaScript file

+

Step 1: Include the JavaScript file

-

It is recommended to include the CSS anchor positioning polyfill to support older browsers. Add this to the <head> of your page:

+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %}{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
diff --git a/docs/src/components/select.njk b/docs/src/components/select.njk index 25eecd8..7591c6e 100644 --- a/docs/src/components/select.njk +++ b/docs/src/components/select.njk @@ -85,15 +85,10 @@ toc:

Step 1: Include the JavaScript files

-

Include the select.js file. It is recommended to also include the CSS anchor positioning polyfill to support older browsers. Add this to the <head> of your page:

+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %} -{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
@@ -205,8 +200,7 @@ toc: {% set code %} {% call select( - listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}, - register_on=["alpine:init", "htmx:afterSwap"] + listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"} ) %} {% for i in range(0, 99) %}
Item {{ i }}
@@ -220,8 +214,7 @@ toc: {% set code %} {% call select( - trigger_attrs={"disabled": "disabled"}, - register_on=["alpine:init", "htmx:afterSwap"] + trigger_attrs={"disabled": "disabled"} ) %}
Disabled
{% endcall %} @@ -233,8 +226,7 @@ toc: {% set code %} {% call select( - trigger_attrs={'class': 'w-[180px]'}, - register_on=["alpine:init", "htmx:afterSwap"] + trigger_attrs={'class': 'w-[180px]'} ) %}
diff --git a/docs/src/components/sidebar.njk b/docs/src/components/sidebar.njk index 09ae050..f8198a2 100644 --- a/docs/src/components/sidebar.njk +++ b/docs/src/components/sidebar.njk @@ -29,10 +29,10 @@ toc:

Step 1: Include the JavaScript files

-

Include the sidebar.js file. Add this to the <head> of your page:

+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %}{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
diff --git a/docs/src/components/tabs.njk b/docs/src/components/tabs.njk index c9ce77d..fec51fd 100644 --- a/docs/src/components/tabs.njk +++ b/docs/src/components/tabs.njk @@ -71,8 +71,7 @@ toc: id='demo-tabs-with-panels', tabsets=tabsets_demo, main_attrs={ "class": "w-full" }, - tablist_attrs={ "class": "w-full" }, - register_on=["alpine:init", "htmx:afterSwap"] + tablist_attrs={ "class": "w-full" } ) }} {% endset %} @@ -83,10 +82,10 @@ toc:

Step 1: Include the JavaScript files

-

Include the tabs.js file. Add this to the <head> of your page:

+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %}{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
@@ -209,7 +208,6 @@ toc: id='demo-tabs-with-panels', tabsets=tabsets_demo, main_attrs={ "class": "w-full" }, - tablist_attrs={ "class": "w-full" }, - register_on=["alpine:init", "htmx:afterSwap"] + tablist_attrs={ "class": "w-full" } ) }}{% endraw %}{% endset%} {{ code_block(raw_code, "jinja") }} \ No newline at end of file diff --git a/docs/src/components/toast.njk b/docs/src/components/toast.njk index 20b2125..864929f 100644 --- a/docs/src/components/toast.njk +++ b/docs/src/components/toast.njk @@ -64,10 +64,10 @@ toc:

Step 1: Include the JavaScript file

-

Include the toast.js file. Add this to the <head> of your page:

+

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %}{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
diff --git a/docs/src/index.njk b/docs/src/index.njk index e36248b..3eb3b85 100644 --- a/docs/src/index.njk +++ b/docs/src/index.njk @@ -53,7 +53,7 @@ layout: layouts/layout.njk {% call select( is_combobox=true, - content_attrs={"data-align": "end"} + popover_attrs={"data-align": "end"} ) %}
Viewer
Developer
@@ -70,7 +70,7 @@ layout: layouts/layout.njk {% call select( is_combobox=true, - content_attrs={"data-align": "end"} + popover_attrs={"data-align": "end"} ) %}
Viewer
Developer
diff --git a/docs/src/installation.njk b/docs/src/installation.njk index c9a7d32..79abef0 100644 --- a/docs/src/installation.njk +++ b/docs/src/installation.njk @@ -3,8 +3,6 @@ layout: layouts/page.njk title: Installation description: How to install and use Basecoat in your app. toc: - - label: Quickstart - id: install-quickstart - label: Install using the CDN id: install-cdn - label: Installing using NPM @@ -33,39 +31,26 @@ toc: {% from "macros/code_block.njk" import code_block %} {% from "tabs.njk" import tabs %} -

Quickstart

+

Install using the CDN

+ +

Install all components

-

The fastest way to get up and running is to load the CSS and all of the components' JavaScript using the CDN. Add the following to the <head> of your HTML file:

+

Add the following to the <head> of your HTML file:

{% set code %} - -{% endset %} +{% endset %} {{ code_block(code, "html") }} -
-

If you're not using all of the components requiring JavaScript (Dropdown Menu, Select, Sidebar, Tabs and Toast), follow the instructions below to only include the JavaScript files you actually need.

-
- -

Install using the CDN

+

Install specific components

-

The easiest way to use Basecoat is use the CDN. First, add the CSS to the <head> of your HTML file:

+

While the JavaScript file for all components is small (around 3kB gzipped), you can cherry-pick the components you need as instructed in the component's page (Dropdown Menu, Popover, Select, Sidebar, Tabs and Toast). For example, to add the Dropdown Menu component, add the following to the <head> of your HTML file:

-{% set code %}{% endset %} -{{ code_block(code, "html") }} - -
-

Depending on the components you want to use, you may also need to add JavaScript files as instructed on the component's page. For example, for the Tabs component, you would add the following:

-
- -{% set code %}{% endset %} +{% set code %} +{% endset %} {{ code_block(code, "html") }}

Install using NPM

@@ -122,20 +107,13 @@ toc:

Components with JavaScript

-

Some components may require JavaScript code to work.

+

A handful of components require JavaScript code to work., specifically Dropdown Menu, Popover, Select, Sidebar, Tabs and Toast

If a component requires JavaScript, the documentation page will provide instructions. There are 2 options to add the JavaScript code to your project:

    -
  • CDN: you add the <script> tag provided on the component's page to the <head> of your HTML file.
  • -
  • Local file: you download the script as a separate file and include it in your project (or let the CLI do it for you). Make sure keep the defer attribute on the <script> tag when doing so.

    -
- -

A few things to keep in mind:

-
diff --git a/docs/src/test.njk b/docs/src/test.njk new file mode 100644 index 0000000..4e32864 --- /dev/null +++ b/docs/src/test.njk @@ -0,0 +1,71 @@ + + + + + + Anchor Positioning Test (Top, End) + + + + + + + +
+ I should be anchored above the button, aligned to the right. +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b9df4af..525ffcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "devDependencies": { "@11ty/eleventy": "^3.0.0", "@grimlink/eleventy-plugin-lucide-icons": "^2.1.4", - "@oddbird/css-anchor-positioning": "^0.6.0", "@tailwindcss/cli": "^4.1.3", "concurrently": "^9.1.2", "prettier": "^3.5.3", @@ -256,34 +255,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", - "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", - "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.1", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "dev": true, - "license": "MIT" - }, "node_modules/@grimlink/eleventy-plugin-lucide-icons": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@grimlink/eleventy-plugin-lucide-icons/-/eleventy-plugin-lucide-icons-2.1.4.tgz", @@ -773,19 +744,6 @@ "node": ">= 8" } }, - "node_modules/@oddbird/css-anchor-positioning": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@oddbird/css-anchor-positioning/-/css-anchor-positioning-0.6.0.tgz", - "integrity": "sha512-5r6AhCHSW/bU45gCwOdaei04WPn2E7oeztC8o0z6euyAPBbAeiYQda5ByIiMRfOf13sa8rVqiCsKpwx/QNyHbw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@floating-ui/dom": "^1.6.13", - "@types/css-tree": "^2.3.10", - "css-tree": "^3.1.0", - "nanoid": "^5.1.5" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -1412,13 +1370,6 @@ "node": ">= 10" } }, - "node_modules/@types/css-tree": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.10.tgz", - "integrity": "sha512-WcaBazJ84RxABvRttQjjFWgTcHvZR9jGr0Y3hccPkHjFyk/a3N8EuxjKr+QfrwjoM5b1yI1Uj1i7EzOAAwBwag==", - "dev": true, - "license": "MIT" - }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -1891,20 +1842,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3158,13 +3095,6 @@ "node": ">=0.10.0" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -3321,25 +3251,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3885,16 +3796,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", diff --git a/package.json b/package.json index 9768e80..3656826 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "devDependencies": { "@11ty/eleventy": "^3.0.0", "@grimlink/eleventy-plugin-lucide-icons": "^2.1.4", - "@oddbird/css-anchor-positioning": "^0.6.0", "@tailwindcss/cli": "^4.1.3", "concurrently": "^9.1.2", "prettier": "^3.5.3", diff --git a/scripts/build.js b/scripts/build.js index 01899e9..c4fdb1e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -119,23 +119,9 @@ async function build() { } } - // Handle anchor positioning polyfill - const polyfillSrc = path.join(projectRoot, 'node_modules', '@oddbird', 'css-anchor-positioning', 'dist', 'css-anchor-positioning.js'); - const polyfillDest = 'css-anchor-positioning.js'; - const polyfillMinDest = 'css-anchor-positioning.min.js'; - - // Copy original polyfill - await fs.copyFile(polyfillSrc, path.join(cliJsDistDir, polyfillDest)); - await fs.copyFile(polyfillSrc, path.join(cssJsDistDir, polyfillDest)); - - // Minify and copy polyfill - const cliPolyfillMinDest = path.join(cliJsDistDir, polyfillMinDest); - await execPromise(`npx terser ${polyfillSrc} -o ${cliPolyfillMinDest} --compress --mangle`); - await fs.copyFile(cliPolyfillMinDest, path.join(cssJsDistDir, polyfillMinDest)); - // Create combined component files console.log('Creating combined component files...'); - const componentsToCombine = ['dropdown-menu.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js']; + const componentsToCombine = ['dropdown-menu.js', 'popover.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js']; const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f)); // Create non-minified bundle diff --git a/src/css/basecoat.css b/src/css/basecoat.css index e587a99..e84b085 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -504,7 +504,9 @@ /* Dropdown Menu */ @layer components { .dropdown-menu { - [popover].popover { + @apply relative inline-flex p-1; + + [data-popover] { @apply p-1; min-width: anchor-size(width); @@ -517,7 +519,7 @@ @apply bg-accent text-accent-foreground; } } - [role='menu'] [role='heading'] { + [role='menu'] [role="presentation"] { @apply flex px-2 py-1.5 text-sm font-medium; } [role='separator'] { @@ -576,85 +578,74 @@ /* Popover */ @layer components { - [popover].popover { - @apply absolute inset-auto bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto p-4 rounded-md border shadow-md z-50 visible opacity-0 scale-95 w-max transition-all transition-discrete; + [data-popover] { + @apply absolute bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto rounded-md border shadow-md z-50 visible opacity-100 scale-100 min-w-full w-max transition-all; - &:not([data-side]), - &[data-side='bottom'] { - @apply -translate-y-2; - } - &[data-side='top'] { - @apply translate-y-2; - } - &[data-side='left'] { - @apply translate-x-2; - } - &[data-side='right'] { - @apply -translate-x-2; + &[aria-hidden='true'] { + @apply invisible opacity-0 scale-95; + + &:not([data-side]), + &[data-side='bottom'] { + @apply -translate-y-2; + } + &[data-side='top'] { + @apply translate-y-2; + } + &[data-side='left'] { + @apply translate-x-2; + } + &[data-side='right'] { + @apply -translate-x-2; + } } &:not([data-side]), &[data-side='bottom'] { - @apply mt-1 top-[anchor(bottom)]; + @apply mt-1 top-full; } &[data-side='top'] { - @apply mb-1 bottom-[anchor(top)]; + @apply mb-1 bottom-full; } &[data-side='left'] { - @apply mr-1 right-[anchor(left)]; + @apply mr-1 right-full; } &[data-side='right'] { - @apply ml-1 left-[anchor(right)]; + @apply ml-1 left-full; } &:not([data-side]), &[data-side='bottom'], &[data-side='top'] { &:not([data-align]), &[data-align='start'] { - @apply left-[anchor(left)]; + @apply left-0; } &[data-align='end'] { - @apply right-[anchor(right)]; + @apply right-0; } &[data-align='center'] { - @apply left-[anchor(center)] -translate-x-1/2; + @apply left-1/2 -translate-x-1/2; } } &[data-side='left'], &[data-side='right'] { &:not([data-align]), &[data-align='start'] { - @apply top-[anchor(top)]; + @apply top-0; } &[data-align='end'] { - @apply bottom-[anchor(bottom)]; + @apply bottom-0; } &[data-align='center'] { - @apply top-[anchor(center)] -translate-y-1/2; + @apply top-1/2 -translate-y-1/2; } } - &:is(:popover-open) { - @apply opacity-100 scale-100 translate-x-0 translate-y-0; - - @starting-style { - @apply opacity-0 scale-95; - - &:not([data-side]), - &[data-side='bottom'] { - @apply -translate-y-2; - } - &[data-side='top'] { - @apply translate-y-2; - } - &[data-side='left'] { - @apply translate-x-2; - } - &[data-side='right'] { - @apply -translate-x-2; - } - } - } - } + .popover { + @apply relative inline-flex; + + [data-popover] { + @apply p-4; + } + } } /* Radio */ @@ -731,8 +722,10 @@ } } *:not(select).select { - [popover].popover { - @apply p-1 min-w-[anchor-size(width)]; + @apply relative inline-flex; + + [data-popover] { + @apply p-1; [role='option'] { @apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm pl-2 py-1.5 pr-7.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate; @@ -741,7 +734,7 @@ @apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem]; } } - [role='listbox'] [role='heading'] { + [role='listbox'] [role="presentation"] { @apply flex text-muted-foreground px-2 py-1.5 text-xs; } [role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) { diff --git a/src/jinja/select.html.jinja b/src/jinja/select.html.jinja index 92f968c..90f0dd5 100644 --- a/src/jinja/select.html.jinja +++ b/src/jinja/select.html.jinja @@ -4,53 +4,66 @@ @param id {string} [optional] - Unique identifier for the select component. @param selected {string} [optional] - The initially selected value. @param name {string} [optional] - The name attribute for the hidden input storing the selected value. - @param items {array} [optional] - An array of items (objects with type, label, value, attrs, and items) to render. @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button. @param content_attrs {object} [optional] - Additional HTML attributes for the popover content div. @param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div. @param input_attrs {object} [optional] - Additional HTML attributes for the hidden input. @param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only). - @param is_combobox {boolean} [optional] [default=False] - Renders a combobox with search functionality if True. + @param is_combobox {boolean} [optional] [default=false] - Renders a combobox with search functionality if true. #} {% macro select( id=None, selected=None, name=None, items=None, - main_attrs={}, trigger_attrs={}, - content_attrs={}, + popover_attrs={}, listbox_attrs={}, - input_attrs={}, search_placeholder="Search entries...", - is_combobox=False + is_combobox=false ) %} -
+{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %} + +{% set first_option = [] %} +{% set selected_option = [] %} + +{% for item in items.items() %} + {% if item.type == "group" %} + {% for sub_item in item.items.items() %} + {% if not first_option[0] %} + {% set first_option = (first_option.push(sub_item), first_option) %} + {% endif %} + {% if selected and sub_item.value == selected and not selected_option[0] %} + {% set selected_option = (selected_option.push(sub_item), selected_option) %} + {% endif %} + {% endfor %} + {% else %} + {% if not first_option[0] %} + {% set first_option = (first_option.push(item), first_option) %} + {% endif %} + {% if selected and item.value == selected and not selected_option[0] %} + {% set selected_option = (selected_option.push(item), selected_option) %} + {% endif %} + {% endif %} +{% endfor %} + +{% set default_option = selected_option[0] or first_option[0] or None %} + +
- {% if name is defined %} - - {% endif %} +
{% endmacro %} @@ -119,29 +129,30 @@ @param items {array} - The array of items to render. @param parent_id_prefix {string} [optional] - The prefix for the item id. #} -{% macro render_select_items(items, parent_id_prefix="items") %} +{% macro render_select_items(items, selected, parent_id_prefix="items") %} {% for item in items.items() %} {% set item_id = parent_id_prefix ~ "-" ~ loop.index %} - {% if item.type == "group" %} {% set group_label_id = item.id if item.id else "group-label-" + item_id %}
{{ item.label }}
- {{ render_select_items(item.items, item_id) if item.items }} + {{ render_select_items(item.items, selected, item_id) if item.items }}
{% elif item.type == "separator" %}
{% elif item.type == "item" or not item.type %}
diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js index dbb2b6f..4a4dc1f 100644 --- a/src/js/dropdown-menu.js +++ b/src/js/dropdown-menu.js @@ -1,13 +1,35 @@ (() => { const initDropdownMenu = (dropdownMenuComponent) => { - const trigger = dropdownMenuComponent.querySelector('[popovertarget]'); - const popover = dropdownMenuComponent.querySelector('[popover]'); - const menu = popover?.querySelector('[role="menu"]'); - if (!trigger || !popover || !menu) return; + const trigger = dropdownMenuComponent.querySelector(':scope > button'); + const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]'); + const menu = popover.querySelector('[role="menu"]'); + + if (!trigger || !menu || !popover) { + console.error('Dropdown menu component is missing a trigger, a menu, or a popover content element.', dropdownMenuComponent); + return; + } let menuItems = []; let activeIndex = -1; + const closeMenu = () => { + if (trigger.getAttribute('aria-expanded') === 'false') return; + trigger.setAttribute('aria-expanded', 'false'); + trigger.removeAttribute('aria-activedescendant'); + popover.setAttribute('aria-hidden', 'true'); + trigger.focus(); + activeIndex = -1; + }; + + const openMenu = () => { + trigger.setAttribute('aria-expanded', 'true'); + popover.setAttribute('aria-hidden', 'false'); + menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])')); + if (menuItems.length > 0) { + setActiveItem(0); + } + }; + const setActiveItem = (index) => { if (activeIndex > -1 && menuItems[activeIndex]) { menuItems[activeIndex].classList.remove('active'); @@ -17,36 +39,48 @@ const activeItem = menuItems[activeIndex]; activeItem.classList.add('active'); trigger.setAttribute('aria-activedescendant', activeItem.id); - activeItem.scrollIntoView({ block: 'nearest' }); } else { trigger.removeAttribute('aria-activedescendant'); } }; - const handleKeyDown = (e) => { - if (!popover.matches(':popover-open')) { + trigger.addEventListener('click', () => { + const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; + if (isExpanded) { + closeMenu(); + } else { + openMenu(); + } + }); + + dropdownMenuComponent.addEventListener('keydown', (e) => { + const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; + + if (e.key === 'Escape') { + if (isExpanded) closeMenu(); + return; + } + + if (!isExpanded) { if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) { e.preventDefault(); - trigger.click(); + openMenu(); } return; } - let nextIndex = activeIndex; if (menuItems.length === 0) return; + let nextIndex = activeIndex; + switch (e.key) { case 'ArrowDown': e.preventDefault(); - if (activeIndex < menuItems.length - 1) { - nextIndex = activeIndex + 1; - } + nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0; break; case 'ArrowUp': e.preventDefault(); - if (activeIndex > 0) { - nextIndex = activeIndex - 1; - } + nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1; break; case 'Home': e.preventDefault(); @@ -60,42 +94,24 @@ case ' ': e.preventDefault(); menuItems[activeIndex]?.click(); - break; + closeMenu(); + return; } if (nextIndex !== activeIndex) { setActiveItem(nextIndex); } - }; - - trigger.addEventListener('keydown', handleKeyDown); - - popover.addEventListener('toggle', (e) => { - trigger.setAttribute('aria-expanded', e.newState === 'open'); - if (e.newState === 'open') { - menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])')); - menuItems.forEach((item, index) => { - if (!item.id) item.id = `${menu.id}-item-${index}`; - }); - setActiveItem(0); - } else { - setActiveItem(-1); - } }); menu.addEventListener('click', (e) => { if (e.target.closest('[role^="menuitem"]')) { - popover.hidePopover(); + closeMenu(); } }); - menu.addEventListener('mouseover', (e) => { - const item = e.target.closest('[role^="menuitem"]:not([disabled])'); - if (item) { - const index = menuItems.indexOf(item); - if (index > -1 && index !== activeIndex) { - setActiveItem(index); - } + document.addEventListener('click', (e) => { + if (!dropdownMenuComponent.contains(e.target)) { + closeMenu(); } }); @@ -107,15 +123,14 @@ const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) { - initDropdownMenu(node); - } - node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu); + if (node.nodeType !== Node.ELEMENT_NODE) return; + if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) { + initDropdownMenu(node); } + node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu); }); }); }); - + observer.observe(document.body, { childList: true, subtree: true }); })(); \ No newline at end of file diff --git a/src/js/popover.js b/src/js/popover.js new file mode 100644 index 0000000..e7eb695 --- /dev/null +++ b/src/js/popover.js @@ -0,0 +1,69 @@ +(() => { + const initPopover = (popoverComponent) => { + const trigger = popoverComponent.querySelector(':scope > button'); + const content = popoverComponent.querySelector(':scope > [data-popover]'); + + if (!trigger || !content) { + console.error('Popover component is missing a trigger button or a content element.', popoverComponent); + return; + } + + const closePopover = () => { + if (trigger.getAttribute('aria-expanded') === 'false') return; + trigger.setAttribute('aria-expanded', 'false'); + content.setAttribute('aria-hidden', 'true'); + trigger.focus(); + }; + + const openPopover = () => { + const elementToFocus = content.querySelector('[autofocus]'); + if (elementToFocus) { + content.addEventListener('transitionend', () => { + elementToFocus.focus(); + }, { once: true }); + } + + trigger.setAttribute('aria-expanded', 'true'); + content.setAttribute('aria-hidden', 'false'); + }; + + trigger.addEventListener('click', () => { + const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; + if (isExpanded) { + closePopover(); + } else { + openPopover(); + } + }); + + popoverComponent.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + closePopover(); + } + }); + + document.addEventListener('click', (e) => { + if (!popoverComponent.contains(e.target)) { + closePopover(); + } + }); + + popoverComponent.dataset.popoverInitialized = true; + }; + + document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover); + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + if (node.matches('.popover:not([data-popover-initialized])')) { + initPopover(node); + } + node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover); + }); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/src/js/select.js b/src/js/select.js index 8a3fdb3..1b02ff4 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -1,8 +1,8 @@ (() => { const initSelect = (selectComponent) => { - const trigger = selectComponent.querySelector(':scope > [popovertarget]'); + const trigger = selectComponent.querySelector(':scope > button'); const selectedValue = trigger.querySelector(':scope > span'); - const popover = selectComponent.querySelector(':scope > [popover]'); + const popover = selectComponent.querySelector(':scope > [data-popover]'); const listbox = popover.querySelector('[role="listbox"]'); const input = selectComponent.querySelector(':scope > input[type="hidden"]'); const filter = selectComponent.querySelector('header input[type="text"]'); @@ -21,15 +21,26 @@ } }; + const closePopover = () => { + popover.setAttribute('aria-hidden', 'true'); + trigger.setAttribute('aria-expanded', 'false'); + if (filter) { + filter.value = ''; + visibleOptions = [...options]; + options.forEach(opt => opt.setAttribute('aria-hidden', 'false')); + } + trigger.removeAttribute('aria-activedescendant'); + if (activeIndex > -1) options[activeIndex]?.classList.remove('active'); + activeIndex = -1; + } + const selectOption = (option) => { if (!option) return; - updateValue(option); - - trigger.removeAttribute('aria-activedescendant'); - options.forEach(opt => opt.classList.remove('active')); - activeIndex = -1; - popover.hidePopover(); + if (option.dataset.value) { + updateValue(option); + } + closePopover(); }; if (filter) { @@ -62,12 +73,14 @@ updateValue(initialOption); const handleKeyNavigation = (e) => { - if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(e.key)) { + const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false'; + + if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) { return; } - if (!popover.matches(':popover-open')) { - if (e.currentTarget === trigger && e.key !== 'Enter') { + if (!isPopoverOpen) { + if (e.key !== 'Enter' && e.key !== 'Escape') { e.preventDefault(); trigger.click(); } @@ -76,9 +89,14 @@ e.preventDefault(); + if (e.key === 'Escape') { + closePopover(); + return; + } + if (e.key === 'Enter') { if (activeIndex > -1) { - selectOption(options[activeIndex]); + selectOption(visibleOptions[activeIndex]); } return; } @@ -130,6 +148,31 @@ filter.addEventListener('keydown', handleKeyNavigation); } + trigger.addEventListener('click', () => { + const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; + + if (isExpanded) { + closePopover(); + } else { + popover.setAttribute('aria-hidden', 'false'); + trigger.setAttribute('aria-expanded', 'true'); + if (filter) filter.focus(); + + const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); + if (selectedOption) { + if (activeIndex > -1) { + options[activeIndex]?.classList.remove('active'); + } + activeIndex = options.indexOf(selectedOption); + selectedOption.classList.add('active'); + if (selectedOption.id) { + trigger.setAttribute('aria-activedescendant', selectedOption.id); + } + selectedOption.scrollIntoView({ block: 'nearest' }); + } + } + }); + listbox.addEventListener('click', (e) => { const clickedOption = e.target.closest('[role="option"]'); if (clickedOption) { @@ -137,46 +180,13 @@ } }); - popover.addEventListener('toggle', (e) => { - trigger.setAttribute('aria-expanded', e.newState === 'open'); - - if (e.newState === 'open') { - if (filter) filter.focus(); - - const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); - let startingOption = null; - - if (selectedOption && visibleOptions.includes(selectedOption)) { - startingOption = selectedOption; - } else if (visibleOptions.length > 0) { - startingOption = visibleOptions[0]; - } - - if (activeIndex > -1) options[activeIndex]?.classList.remove('active'); - - if (startingOption) { - activeIndex = options.indexOf(startingOption); - startingOption.classList.add('active'); - if (startingOption.id) { - trigger.setAttribute('aria-activedescendant', startingOption.id); - } - startingOption.scrollIntoView({ block: 'nearest' }); - } else { - activeIndex = -1; - } - } else if (e.newState === 'closed') { - if (filter) { - filter.value = ''; - visibleOptions = [...options]; - options.forEach(opt => opt.setAttribute('aria-hidden', 'false')); - } - - trigger.removeAttribute('aria-activedescendant'); - if (activeIndex > -1) options[activeIndex]?.classList.remove('active'); - activeIndex = -1; + document.addEventListener('click', (e) => { + if (!selectComponent.contains(e.target)) { + closePopover(); } }); + popover.setAttribute('aria-hidden', 'true'); selectComponent.dataset.selectInitialized = true; }; diff --git a/src/nunjucks/dialog.njk b/src/nunjucks/dialog.njk index 369d3aa..31cacac 100644 --- a/src/nunjucks/dialog.njk +++ b/src/nunjucks/dialog.njk @@ -8,10 +8,10 @@ @param footer {string} [optional] - HTML content for the dialog footer. @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button. - @param content_attrs {object} [optional] - Additional HTML attributes for the dialog content article. - @param content_header_attrs {object} [optional] - Additional HTML attributes for the dialog header. - @param content_body_attrs {object} [optional] - Additional HTML attributes for the dialog body section. - @param content_footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer. + @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article. + @param header_attrs {object} [optional] - Additional HTML attributes for the dialog header. + @param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section. + @param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer. @param open {boolean} [optional] [default=false] - Whether the dialog should be open initially. @param close_button {boolean} [optional] [default=true] - Whether to include a close button. @param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog. diff --git a/src/nunjucks/dropdown-menu.njk b/src/nunjucks/dropdown-menu.njk index afc1a66..205f558 100644 --- a/src/nunjucks/dropdown-menu.njk +++ b/src/nunjucks/dropdown-menu.njk @@ -6,23 +6,28 @@ @param menu {array} [optional] - Array of menu items for the dropdown. @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button. - @param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div. + @param popover_attrs {object} [optional] - Additional HTML attributes for the dropdown content div. #} {% macro dropdown_menu( id=None, trigger, items=None, + main_attrs={}, trigger_attrs={}, popover_attrs={}, menu_attrs={} ) %} {% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %} - {% endmacro %} @@ -77,7 +82,7 @@ {{ key }}="{{ value }}" {% endfor %} > -
{{ item.label }}
+ {{ render_dropdown_items(item.items, item_id) if item.items }}
{% elif item.type == "separator" %} @@ -85,6 +90,7 @@ {% elif item.type == "item" or not item.type %} {% if item.url %}
{% else %} - +
{% endif %} {% endif %} {% endfor %} diff --git a/src/nunjucks/popover.njk b/src/nunjucks/popover.njk index b700b24..239d2ce 100644 --- a/src/nunjucks/popover.njk +++ b/src/nunjucks/popover.njk @@ -5,35 +5,43 @@ @param trigger {string} [optional] - HTML content for the element that triggers the popover. @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element. - @param content_attrs {object} [optional] - Additional HTML attributes for the popover content div. + @param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div. #} {% macro popover( id=None, trigger, + main_attrs={}, trigger_attrs={}, popover_attrs={} ) %} {% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %} - +
- {{ caller() if caller }} +> + +
{% endmacro %} \ No newline at end of file diff --git a/src/nunjucks/select.njk b/src/nunjucks/select.njk index 0d75439..a4e027b 100644 --- a/src/nunjucks/select.njk +++ b/src/nunjucks/select.njk @@ -6,7 +6,7 @@ @param name {string} [optional] - The name attribute for the hidden input storing the selected value. @param main_attrs {object} [optional] - Additional HTML attributes for the main container div. @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button. - @param content_attrs {object} [optional] - Additional HTML attributes for the popover content div. + @param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div. @param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div. @param input_attrs {object} [optional] - Additional HTML attributes for the hidden input. @param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only). @@ -17,9 +17,11 @@ selected=None, name=None, items=None, + main_attrs={}, trigger_attrs={}, popover_attrs={}, listbox_attrs={}, + input_attrs={}, search_placeholder="Search entries...", is_combobox=false ) %} @@ -28,34 +30,40 @@ {% set first_option = [] %} {% set selected_option = [] %} -{% for item in items %} - {% if item.type == "group" %} - {% for sub_item in item.items %} +{% if items %} + {% for item in items %} + {% if item.type == "group" %} + {% for sub_item in item.items %} + {% if not first_option[0] %} + {% set first_option = (first_option.push(sub_item), first_option) %} + {% endif %} + {% if selected and sub_item.value == selected and not selected_option[0] %} + {% set selected_option = (selected_option.push(sub_item), selected_option) %} + {% endif %} + {% endfor %} + {% else %} {% if not first_option[0] %} - {% set first_option = (first_option.push(sub_item), first_option) %} + {% set first_option = (first_option.push(item), first_option) %} {% endif %} - {% if selected and sub_item.value == selected and not selected_option[0] %} - {% set selected_option = (selected_option.push(sub_item), selected_option) %} + {% if selected and item.value == selected and not selected_option[0] %} + {% set selected_option = (selected_option.push(item), selected_option) %} {% endif %} - {% endfor %} - {% else %} - {% if not first_option[0] %} - {% set first_option = (first_option.push(item), first_option) %} {% endif %} - {% if selected and item.value == selected and not selected_option[0] %} - {% set selected_option = (selected_option.push(item), selected_option) %} - {% endif %} - {% endif %} -{% endfor %} + {% endfor %} +{% endif %} {% set default_option = selected_option[0] or first_option[0] or None %} -
+