diff --git a/.storybook/index.css b/.storybook/index.css
index afd05bdc..44ff93bf 100644
--- a/.storybook/index.css
+++ b/.storybook/index.css
@@ -15,6 +15,7 @@ body {
.light .sbdocs .sbdocs-content > h2,
.light .sbdocs .sbdocs-content > h3,
.light .sbdocs .sbdocs-content > .sb-anchor > h3,
+.light .sbdocs .sbdocs-content > .sb-anchor > p,
.light .sbdocs .sbdocs-content > p,
.light .sbdocs .sbdocs-content > table th,
.light .sbdocs .sbdocs-content > table td {
@@ -25,6 +26,7 @@ body {
.dark .sbdocs .sbdocs-content > h2,
.dark .sbdocs .sbdocs-content > h3,
.dark .sbdocs .sbdocs-content > .sb-anchor > h3,
+.dark .sbdocs .sbdocs-content > .sb-anchor > p,
.dark .sbdocs .sbdocs-content > p,
.dark .sbdocs .sbdocs-content > table th,
.dark .sbdocs .sbdocs-content > table td {
@@ -163,6 +165,14 @@ body {
@apply mt-2;
}
+.dark .sb-anchor > p > a {
+ @apply text-green-400;
+}
+
+.light .sb-anchor > p > a {
+ @apply text-green-600;
+}
+
.sb-bar,
.docs-story {
@apply bg-white;
diff --git a/src/assets/icons/vueless/arrow_right.svg b/src/assets/icons/vueless/arrow_right.svg
new file mode 100644
index 00000000..8f3ecf32
--- /dev/null
+++ b/src/assets/icons/vueless/arrow_right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/constants.js b/src/constants.js
index 09af06ac..3ef93443 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -147,6 +147,7 @@ export const COMPONENTS = {
UTabs: "ui.navigation-tabs",
UProgress: "ui.navigation-progress",
UPagination: "ui.navigation-pagination",
+ UBreadcrumbs: "ui.navigation-breadcrumbs",
/* Loaders and Skeletons */
ULoader: "ui.loader",
diff --git a/src/types.ts b/src/types.ts
index 1808cb3e..152fa646 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -37,6 +37,7 @@ import UPaginationConfig from "./ui.navigation-pagination/config.ts";
import UProgressConfig from "./ui.navigation-progress/config.ts";
import UTabConfig from "./ui.navigation-tab/config.ts";
import UTabsConfig from "./ui.navigation-tabs/config.ts";
+import UBreadcrumbsConfig from "./ui.navigation-breadcrumbs/config.ts";
import UAvatarConfig from "./ui.image-avatar/config.ts";
import UIconConfig from "./ui.image-icon/config.ts";
import UCheckboxConfig from "./ui.form-checkbox/config.ts";
@@ -226,6 +227,7 @@ export interface Components {
UProgress: Partial;
UTab: Partial;
UTabs: Partial;
+ UBreadcrumbs: Partial;
UAvatar: Partial;
UIcon: Partial;
UCheckbox: Partial;
diff --git a/src/ui.button-link/ULink.vue b/src/ui.button-link/ULink.vue
index 0d187463..42ed1126 100644
--- a/src/ui.button-link/ULink.vue
+++ b/src/ui.button-link/ULink.vue
@@ -9,7 +9,7 @@ import { getDefaults } from "../utils/ui.ts";
import defaultConfig from "./config.ts";
import { COMPONENT_NAME } from "./constants.ts";
-import type { Props, Config } from "./types.ts";
+import type { Props, Config, ULinkSlotProps } from "./types.ts";
defineOptions({ inheritAttrs: false });
@@ -101,6 +101,7 @@ const { getDataTest, linkAttrs } = useUI(defaultConfig, mutatedProps);
(defaultConfig, mutatedProps);
@keydown="onKeydown"
@mouseover="onMouseover"
>
-
- {{ label }}
+
+
+ {{ label }}
+
+import { computed } from "vue";
+
+import useUI from "../composables/useUI.ts";
+import { getDefaults } from "../utils/ui.ts";
+
+import defaultConfig from "./config.ts";
+import { COMPONENT_NAME } from "./constants.ts";
+
+import ULink from "../ui.button-link/ULink.vue";
+import UIcon from "../ui.image-icon/UIcon.vue";
+
+import type { Props, Config, UBreadcrumb } from "./types.ts";
+import type { ULinkSlotProps } from "../ui.button-link/types.ts";
+
+defineOptions({ inheritAttrs: false });
+
+const props = withDefaults(defineProps(), {
+ ...getDefaults(defaultConfig, COMPONENT_NAME),
+ links: () => [],
+});
+
+const emit = defineEmits([
+ /**
+ * Triggers on a link click.
+ * @property {object} link
+ */
+ "clickLink",
+]);
+
+const getIconColor = computed(() => {
+ return (link: UBreadcrumb) => (link.disabled || (!link.to && !link.href) ? "gray" : props.color);
+});
+
+function onClickLink(link: UBreadcrumb) {
+ emit("clickLink", link);
+}
+
+/**
+ * Get element / nested component attributes for each config token ✨
+ * Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
+ */
+const { config, breadcrumbsAttrs, breadcrumbLinkAttrs, breadcrumbIconAttrs, dividerIconAttrs } =
+ useUI(defaultConfig);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui.navigation-breadcrumbs/config.ts b/src/ui.navigation-breadcrumbs/config.ts
new file mode 100644
index 00000000..86c6c78b
--- /dev/null
+++ b/src/ui.navigation-breadcrumbs/config.ts
@@ -0,0 +1,32 @@
+export default /*tw*/ {
+ breadcrumbs: "flex items-center gap-1 py-2",
+ breadcrumbLink: "{ULink}",
+ breadcrumbIcon: {
+ base: "{UIcon}",
+ defaults: {
+ size: {
+ sm: "2xs",
+ md: "xs",
+ lg: "sm",
+ },
+ },
+ },
+ dividerIcon: {
+ base: "{UIcon}",
+ defaults: {
+ size: {
+ sm: "xs",
+ md: "sm",
+ lg: "md",
+ },
+ },
+ },
+ defaults: {
+ color: "grayscale",
+ size: "md",
+ underlined: undefined,
+ dashed: false,
+ target: "_self",
+ dividerIcon: "arrow_right",
+ },
+};
diff --git a/src/ui.navigation-breadcrumbs/constants.ts b/src/ui.navigation-breadcrumbs/constants.ts
new file mode 100644
index 00000000..16ef20c4
--- /dev/null
+++ b/src/ui.navigation-breadcrumbs/constants.ts
@@ -0,0 +1,5 @@
+/*
+ This const is needed to prevent the issue in script setup:
+ `defineProps` is referencing locally declared variables. (vue/valid-define-props)
+ */
+export const COMPONENT_NAME = "UBreadcrumbs";
diff --git a/src/ui.navigation-breadcrumbs/storybook/docs.mdx b/src/ui.navigation-breadcrumbs/storybook/docs.mdx
new file mode 100644
index 00000000..4dc3d28b
--- /dev/null
+++ b/src/ui.navigation-breadcrumbs/storybook/docs.mdx
@@ -0,0 +1,37 @@
+import { Markdown, Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/blocks";
+import { getSource } from "../../utils/storybook.ts";
+
+import * as stories from "./stories.ts";
+import defaultConfig from "../config.ts?raw"
+
+
+
+
+
+
+
+
+
+## Breadcrumb Link Object Properties
+Keys you may/have to provide to the component in a `link` object.
+
+
+{`
+| Key name | Description | Type |
+| ---------------------- | ----------------------------------------------------- | ----------------------- |
+| label | Link label | String |
+| route | Route object | Object |
+| href | Link URL | String |
+| disabled | Used to disable link | Boolean |
+| icon | Icon name | String |
+| target | Specifies where to open the linked page. | String |
+| custom | Whether RouterLink should not wrap content in a tag | Boolean |
+| replace | Whether to replace current history entry | Boolean |
+| activeClass | Classes to apply when route is active | String |
+| exactActiveClass | Classes to apply when route is exactly active | String |
+| ariaCurrentValue | Value for aria-current when link is exact active | String |
+`}
+
+
+## Default config
+
diff --git a/src/ui.navigation-breadcrumbs/storybook/stories.ts b/src/ui.navigation-breadcrumbs/storybook/stories.ts
new file mode 100644
index 00000000..01e0c5ad
--- /dev/null
+++ b/src/ui.navigation-breadcrumbs/storybook/stories.ts
@@ -0,0 +1,156 @@
+import { getArgTypes, getSlotNames, getSlotsFragment } from "../../utils/storybook.ts";
+
+import UBreadcrumbs from "../../ui.navigation-breadcrumbs/UBreadcrumbs.vue";
+import UCol from "../../ui.container-col/UCol.vue";
+import UBadge from "../../ui.text-badge/UBadge.vue";
+import UButton from "../../ui.button/UButton.vue";
+
+import type { Meta, StoryFn } from "@storybook/vue3";
+import type { Props } from "../types.ts";
+
+interface UBreadcrumbsArgs extends Props {
+ slotTemplate?: string;
+ enum: "size";
+}
+
+interface UBreadcrumbsArgs extends Props {
+ slotTemplate?: string;
+}
+
+export default {
+ id: "8030",
+ title: "Navigation / Breadcrumbs",
+ component: UBreadcrumbs,
+ args: {
+ links: [
+ { label: "Vueless Docs", href: "https://docs.vueless.com/" },
+ { label: "Global Customization", href: "https://docs.vueless.com/global-customization/" },
+ { label: "Rounding", href: "https://docs.vueless.com/global-customization/rounding" },
+ ],
+ target: "_blank",
+ },
+ argTypes: {
+ ...getArgTypes(UBreadcrumbs.__name),
+ },
+} as Meta;
+
+const DefaultTemplate: StoryFn = (args: UBreadcrumbsArgs) => ({
+ components: { UBreadcrumbs },
+ setup() {
+ const slots = getSlotNames(UBreadcrumbs.__name);
+
+ return { args, slots };
+ },
+ template: `
+
+ ${args.slotTemplate || getSlotsFragment("")}
+
+ `,
+});
+
+const EnumVariantTemplate: StoryFn = (args: UBreadcrumbsArgs, { argTypes }) => ({
+ components: { UBreadcrumbs, UCol },
+ setup() {
+ return {
+ args,
+ options: argTypes?.[args.enum]?.options,
+ };
+ },
+ template: `
+
+
+
+ `,
+});
+
+export const Default = DefaultTemplate.bind({});
+Default.args = {};
+
+export const Sizes = EnumVariantTemplate.bind({});
+Sizes.args = { enum: "size" };
+
+export const Styles = DefaultTemplate.bind({});
+Styles.args = { color: "green", dashed: true };
+Styles.parameters = {
+ docs: {
+ description: {
+ story:
+ "For a full list of ULink's supported colors, underlined/dashed styles, etc., see the [ULink Documentation](https://ui.vueless.com/?path=/docs/1060--docs).",
+ },
+ },
+};
+
+export const LinkStates = DefaultTemplate.bind({});
+LinkStates.args = {
+ links: [
+ { label: "Default link", href: "https://vueless.com/" },
+ { label: "Empty link (no `route` or `href` properties)" },
+ {
+ label: "Manually disabled link",
+ href: "https://docs.vueless.com/",
+ disabled: true,
+ },
+ ],
+};
+LinkStates.parameters = {
+ docs: {
+ description: {
+ story:
+ // eslint-disable-next-line vue/max-len
+ "A breadcrumb is automatically disabled, if:
- it does not have both `route` and `href` properties;
- it has `disabled` property set to `true.",
+ },
+ },
+};
+
+export const LinkIcon = DefaultTemplate.bind({});
+LinkIcon.args = {
+ links: [
+ { label: "Vueless", href: "https://vueless.com/", icon: "palette" },
+ { label: "Settings", icon: "settings" },
+ { label: "Breadcrumbs", href: "https://ui.vueless.com/?path=/docs/8030--docs" },
+ ],
+};
+LinkIcon.parameters = {
+ docs: {
+ description: {
+ story:
+ "You can pass an icon for a specific breadcrumb in the `links` array via the `icon` property.",
+ },
+ },
+};
+
+export const Slots: StoryFn = (args) => ({
+ components: { UBreadcrumbs, UBadge, UButton },
+ setup() {
+ return { args };
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /
+
+
+ `,
+});
diff --git a/src/ui.navigation-breadcrumbs/types.ts b/src/ui.navigation-breadcrumbs/types.ts
new file mode 100644
index 00000000..736118c7
--- /dev/null
+++ b/src/ui.navigation-breadcrumbs/types.ts
@@ -0,0 +1,72 @@
+import defaultConfig from "./config.ts";
+import type { ComponentConfig } from "../types.ts";
+import type { Props as ULinkProps } from "../ui.button-link/types.ts";
+
+export type Config = typeof defaultConfig;
+
+export interface UBreadcrumb extends ULinkProps {
+ icon?: string;
+}
+
+export interface Props {
+ /**
+ * Array of links.
+ */
+ links?: UBreadcrumb[];
+
+ /**
+ * Breadcrumbs' size.
+ */
+ size?: "sm" | "md" | "lg";
+
+ /**
+ * Breadcrumbs' color.
+ */
+ color?:
+ | "grayscale"
+ | "red"
+ | "orange"
+ | "amber"
+ | "yellow"
+ | "lime"
+ | "green"
+ | "emerald"
+ | "teal"
+ | "cyan"
+ | "sky"
+ | "blue"
+ | "indigo"
+ | "violet"
+ | "purple"
+ | "fuchsia"
+ | "pink"
+ | "rose"
+ | "gray"
+ | "white"
+ | "brand";
+
+ /**
+ * Specifies where to open the linked page.
+ */
+ target?: "_blank" | "_self" | "_parent" | "_top" | string;
+
+ /**
+ * Show underline.
+ */
+ underlined?: boolean;
+
+ /**
+ * Set breadcrumbs' underline style as dashed.
+ */
+ dashed?: boolean;
+
+ /**
+ * Component config object.
+ */
+ config?: ComponentConfig;
+
+ /**
+ * Data-test attribute for automated testing.
+ */
+ dataTest?: string;
+}