@db-vincent/components 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/package.json +28 -4
  2. package/src/astro/Accordion.astro +37 -0
  3. package/src/astro/Alert.astro +73 -0
  4. package/src/astro/Avatar.astro +88 -0
  5. package/src/astro/AvatarGroup.astro +63 -0
  6. package/src/astro/Badge.astro +23 -0
  7. package/src/astro/Breadcrumbs.astro +45 -0
  8. package/src/astro/Button.astro +125 -0
  9. package/src/astro/Card.astro +63 -0
  10. package/src/astro/Checkbox.astro +61 -0
  11. package/src/astro/Chip.astro +135 -0
  12. package/src/astro/CodeBlock.astro +17 -0
  13. package/src/astro/Drawer.astro +30 -0
  14. package/src/astro/EmptyState.astro +45 -0
  15. package/src/astro/Example.astro +37 -0
  16. package/src/astro/Grid.astro +56 -0
  17. package/src/astro/GridItem.astro +25 -0
  18. package/src/astro/Icon.astro +52 -0
  19. package/src/astro/Menu.astro +151 -0
  20. package/src/astro/MenuItem.astro +57 -0
  21. package/src/astro/Modal.astro +124 -0
  22. package/src/astro/Navbar.astro +756 -0
  23. package/src/astro/PageHeader.astro +34 -0
  24. package/src/astro/Pagination.astro +212 -0
  25. package/src/astro/Popover.astro +51 -0
  26. package/src/astro/Progress.astro +71 -0
  27. package/src/astro/Radio.astro +58 -0
  28. package/src/astro/RadioGroup.astro +25 -0
  29. package/src/astro/Section.astro +25 -0
  30. package/src/astro/Select.astro +420 -0
  31. package/src/astro/Sidebar.astro +215 -0
  32. package/src/astro/Skeleton.astro +41 -0
  33. package/src/astro/Spinner.astro +20 -0
  34. package/src/astro/Stepper.astro +82 -0
  35. package/src/astro/Switch.astro +78 -0
  36. package/src/astro/Table.astro +268 -0
  37. package/src/astro/Tabs.astro +83 -0
  38. package/src/astro/TextField.astro +305 -0
  39. package/src/astro/ThemeToggle.astro +66 -0
  40. package/src/astro/Toast.astro +181 -0
  41. package/src/astro/Tooltip.astro +54 -0
  42. package/src/astro/TopBar.astro +29 -0
  43. package/src/data/icons.ts +120 -0
  44. package/src/scripts/avatar.ts +33 -0
  45. package/src/scripts/menu.ts +218 -0
  46. package/src/scripts/tabs.ts +127 -0
  47. package/src/scripts/toast.ts +565 -0
  48. package/src/scripts/tooltip.ts +127 -0
  49. package/src/styles/accordion.css +79 -0
  50. package/src/styles/alert.css +108 -0
  51. package/src/styles/avatar.css +131 -0
  52. package/src/styles/badge.css +48 -0
  53. package/src/styles/base.css +205 -0
  54. package/src/styles/breadcrumbs.css +54 -0
  55. package/src/styles/button.css +174 -0
  56. package/src/styles/card.css +112 -0
  57. package/src/styles/chip.css +106 -0
  58. package/src/styles/choice.css +143 -0
  59. package/src/styles/code.css +44 -0
  60. package/src/styles/empty.css +59 -0
  61. package/src/styles/example.css +57 -0
  62. package/src/styles/grid.css +64 -0
  63. package/src/styles/input.css +208 -0
  64. package/src/styles/menu.css +151 -0
  65. package/src/styles/modal.css +285 -0
  66. package/src/styles/motion.css +114 -0
  67. package/src/styles/nav.css +232 -0
  68. package/src/styles/navbar.css +658 -0
  69. package/src/styles/page-header.css +62 -0
  70. package/src/styles/pagination.css +81 -0
  71. package/src/styles/popover.css +134 -0
  72. package/src/styles/progress.css +95 -0
  73. package/src/styles/section.css +38 -0
  74. package/src/styles/select.css +246 -0
  75. package/src/styles/skeleton.css +66 -0
  76. package/src/styles/spinner.css +44 -0
  77. package/src/styles/sr-only.css +14 -0
  78. package/src/styles/stepper.css +169 -0
  79. package/src/styles/switch.css +143 -0
  80. package/src/styles/table.css +177 -0
  81. package/src/styles/tabs.css +120 -0
  82. package/src/styles/theme-toggle.css +31 -0
  83. package/src/styles/toast.css +225 -0
  84. package/src/styles/tooltip.css +63 -0
  85. package/src/styles/topbar.css +32 -0
  86. package/README.md +0 -3
@@ -0,0 +1,135 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/chip.css";
4
+
5
+ /**
6
+ * Chip. Renders a span by default. `interactive` renders a toggle button with
7
+ * aria-pressed; `removable` adds a separate remove button with its own label.
8
+ */
9
+ interface Props {
10
+ interactive?: boolean;
11
+ selected?: boolean;
12
+ removable?: boolean;
13
+ disabled?: boolean;
14
+ removeLabel?: string;
15
+ href?: string;
16
+ size?: "sm" | "md";
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ interactive = false,
22
+ selected = false,
23
+ removable = false,
24
+ disabled = false,
25
+ removeLabel,
26
+ href,
27
+ size = "md",
28
+ class: className,
29
+ } = Astro.props;
30
+
31
+ // The remove button names the item it removes ("Remove 4K"), not just "Remove".
32
+ // The slot HTML is already entity-escaped and Astro escapes the attribute
33
+ // again, so decode before it reaches aria-label.
34
+ const ENTITIES: Record<string, string> = {
35
+ amp: "&",
36
+ lt: "<",
37
+ gt: ">",
38
+ quot: '"',
39
+ apos: "'",
40
+ nbsp: " ",
41
+ };
42
+ const decode = (html: string) =>
43
+ html.replace(
44
+ /&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi,
45
+ (match, dec, hex, name) => {
46
+ if (name) return ENTITIES[name.toLowerCase()] ?? match;
47
+ const code = Number.parseInt(dec ?? hex, dec ? 10 : 16);
48
+ return code <= 0x10ffff ? String.fromCodePoint(code) : match;
49
+ },
50
+ );
51
+ const text = decode(
52
+ (await Astro.slots.render("default")).replace(/<[^>]+>/g, ""),
53
+ ).trim();
54
+ const removeName = removeLabel ?? (text ? `Remove ${text}` : "Remove");
55
+
56
+ const classes = [
57
+ "ds-chip",
58
+ size === "sm" && "ds-chip--sm",
59
+ selected && "is-selected",
60
+ disabled && "is-disabled",
61
+ className,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(" ");
65
+ ---
66
+
67
+ {removable ? (
68
+ <span class={classes}>
69
+ <slot />
70
+ <button
71
+ class="ds-chip__remove"
72
+ type="button"
73
+ aria-label={removeName}
74
+ disabled={disabled}
75
+ data-chip-remove
76
+ >
77
+ <Icon name="x" size={14} />
78
+ </button>
79
+ </span>
80
+ ) : href ? (
81
+ <a class={classes} href={href}>
82
+ <slot />
83
+ </a>
84
+ ) : interactive ? (
85
+ <button
86
+ class={classes}
87
+ type="button"
88
+ aria-pressed={selected ? "true" : "false"}
89
+ disabled={disabled}
90
+ data-chip-toggle
91
+ >
92
+ <Icon name="check" size={14} class="ds-chip__check" />
93
+ <slot />
94
+ </button>
95
+ ) : (
96
+ <span class={classes}>
97
+ <slot />
98
+ </span>
99
+ )}
100
+
101
+ <script>
102
+ document.addEventListener("click", (event) => {
103
+ const target = event.target as HTMLElement;
104
+ const remove = target.closest<HTMLButtonElement>("[data-chip-remove]");
105
+ if (remove) {
106
+ const chip = remove.closest<HTMLElement>(".ds-chip");
107
+ if (!chip) return;
108
+ const focusable = (el: Element | null) =>
109
+ el?.matches("[data-chip-toggle]")
110
+ ? (el as HTMLElement)
111
+ : el?.querySelector<HTMLElement>(
112
+ "[data-chip-remove], [data-chip-toggle]",
113
+ );
114
+ const target =
115
+ focusable(chip.nextElementSibling) ??
116
+ focusable(chip.previousElementSibling);
117
+ const fallback = chip.parentElement;
118
+ chip.remove();
119
+ if (target) {
120
+ target.focus();
121
+ } else if (fallback) {
122
+ fallback.tabIndex = -1;
123
+ fallback.focus();
124
+ }
125
+ return;
126
+ }
127
+
128
+ const toggle = target.closest<HTMLButtonElement>("[data-chip-toggle]");
129
+ if (!toggle || toggle.disabled) return;
130
+ toggle.setAttribute(
131
+ "aria-pressed",
132
+ toggle.getAttribute("aria-pressed") === "true" ? "false" : "true",
133
+ );
134
+ });
135
+ </script>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import "../styles/code.css";
3
+
4
+ /** Code sample. Renders the exact source passed in; no highlighting tricks. */
5
+ interface Props {
6
+ code?: string;
7
+ label?: string;
8
+ class?: string;
9
+ }
10
+
11
+ const { code, label, class: className } = Astro.props;
12
+ ---
13
+
14
+ <figure class:list={["ds-code", className]}>
15
+ {label && <figcaption class="ds-code__label">{label}</figcaption>}
16
+ <pre class="ds-code__pre"><code>{code ?? <slot />}</code></pre>
17
+ </figure>
@@ -0,0 +1,30 @@
1
+ ---
2
+ import Modal from "./Modal.astro";
3
+
4
+ /**
5
+ * Side drawer: a Modal anchored to the `start` or `end` edge (or `bottom`).
6
+ * Same native <dialog> behaviour, styles and script as Modal; this wrapper only
7
+ * changes the defaults.
8
+ */
9
+ interface Props {
10
+ id: string;
11
+ title: string;
12
+ description?: string;
13
+ triggerLabel?: string;
14
+ triggerVariant?: "primary" | "secondary" | "ghost" | "danger";
15
+ size?: "sm" | "md" | "lg";
16
+ placement?: "start" | "end" | "bottom";
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ triggerLabel = "Open drawer",
22
+ placement = "end",
23
+ ...rest
24
+ } = Astro.props;
25
+ ---
26
+
27
+ <Modal {...rest} triggerLabel={triggerLabel} placement={placement}>
28
+ <slot />
29
+ <slot name="footer" slot="footer" />
30
+ </Modal>
@@ -0,0 +1,45 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import Skeleton from "./Skeleton.astro";
4
+ import "../styles/empty.css";
5
+
6
+ /**
7
+ * Empty state. Says what is missing, why it matters, and the next action.
8
+ * `dashed` frames the region; `skeleton` previews the shape of the content
9
+ * that will replace it.
10
+ */
11
+ interface Props {
12
+ icon?: string;
13
+ title: string;
14
+ message: string;
15
+ dashed?: boolean;
16
+ skeleton?: boolean;
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ icon = "inbox",
22
+ title,
23
+ message,
24
+ dashed = false,
25
+ skeleton = false,
26
+ class: className,
27
+ } = Astro.props;
28
+ ---
29
+
30
+ <div class:list={["ds-empty", dashed && "ds-empty--dashed", className]}>
31
+ <span class="ds-empty__icon" aria-hidden="true">
32
+ <Icon name={icon} size={24} />
33
+ </span>
34
+ {skeleton && (
35
+ <div class="ds-empty__skeleton" aria-hidden="true">
36
+ <Skeleton variant="heading" />
37
+ <Skeleton lines={2} />
38
+ </div>
39
+ )}
40
+ <h3 class="ds-empty__title">{title}</h3>
41
+ <p class="ds-empty__body">{message}</p>
42
+ <div class="ds-empty__actions">
43
+ <slot />
44
+ </div>
45
+ </div>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import CodeBlock from "./CodeBlock.astro";
3
+ import "../styles/example.css";
4
+
5
+ /**
6
+ * A live example and the exact source that produced it. Every specimen in the
7
+ * explorer goes through this component so no demo can drift from its code.
8
+ */
9
+ interface Props {
10
+ title: string;
11
+ code: string;
12
+ description?: string;
13
+ flush?: boolean;
14
+ class?: string;
15
+ }
16
+
17
+ const {
18
+ title,
19
+ code,
20
+ description,
21
+ flush = false,
22
+ class: className,
23
+ } = Astro.props;
24
+ ---
25
+
26
+ <figure class:list={["ds-example", className]}>
27
+ <figcaption class="ds-example__caption">
28
+ <span class="ds-example__title">{title}</span>
29
+ {description && <span class="ds-example__description">{description}</span>}
30
+ </figcaption>
31
+ <div
32
+ class:list={["ds-example__preview", flush && "ds-example__preview--flush"]}
33
+ >
34
+ <slot />
35
+ </div>
36
+ <CodeBlock label="Astro" code={code} />
37
+ </figure>
@@ -0,0 +1,56 @@
1
+ ---
2
+ import "../styles/grid.css";
3
+
4
+ interface Props {
5
+ columns?: number;
6
+ phoneColumns?: number;
7
+ gap?: "sm" | "md" | "lg";
8
+ auto?: boolean;
9
+ masonry?: boolean;
10
+ minItem?: string;
11
+ container?: boolean;
12
+ as?: string;
13
+ class?: string;
14
+ }
15
+
16
+ const {
17
+ columns = 12,
18
+ phoneColumns = 1,
19
+ gap = "md",
20
+ auto = false,
21
+ masonry = false,
22
+ minItem = "16rem",
23
+ container = false,
24
+ as: Tag = "div",
25
+ class: className,
26
+ } = Astro.props;
27
+
28
+ const safeLength = (v: string | undefined, fallback?: string) =>
29
+ v && /^\d+(\.\d+)?(rem|em|px|%|ch|vw|vh)$/.test(v) ? v : fallback;
30
+ const safeInt = (n: number, fallback: number) =>
31
+ Number.isFinite(n) ? Math.max(1, Math.floor(n)) : fallback;
32
+ const cols = safeInt(columns, 12);
33
+ const phoneCols = safeInt(phoneColumns, 1);
34
+ const style = `--grid-columns:${cols};--grid-columns-phone:${phoneCols};--grid-min:${safeLength(minItem, "16rem")};--grid-masonry-columns:${cols}`;
35
+ const classes = [
36
+ "ds-grid",
37
+ gap !== "md" && `ds-grid--gap-${gap}`,
38
+ auto && "ds-grid--auto",
39
+ masonry && "ds-grid--masonry",
40
+ className,
41
+ ]
42
+ .filter(Boolean)
43
+ .join(" ");
44
+ ---
45
+
46
+ {container ? (
47
+ <div class="ds-grid__container">
48
+ <Tag class={classes} style={style}>
49
+ <slot />
50
+ </Tag>
51
+ </div>
52
+ ) : (
53
+ <Tag class={classes} style={style}>
54
+ <slot />
55
+ </Tag>
56
+ )}
@@ -0,0 +1,25 @@
1
+ ---
2
+ import "../styles/grid.css";
3
+
4
+ interface Props {
5
+ span?: number;
6
+ phoneSpan?: number;
7
+ as?: string;
8
+ class?: string;
9
+ }
10
+
11
+ const {
12
+ span = 1,
13
+ phoneSpan = 1,
14
+ as: Tag = "div",
15
+ class: className,
16
+ } = Astro.props;
17
+
18
+ const safeInt = (n: number, fallback: number) =>
19
+ Number.isFinite(n) ? Math.max(1, Math.floor(n)) : fallback;
20
+ const style = `--grid-item-span:${safeInt(span, 1)};--grid-item-span-phone:${safeInt(phoneSpan, 1)}`;
21
+ ---
22
+
23
+ <Tag class:list={["ds-grid__item", className]} style={style}>
24
+ <slot />
25
+ </Tag>
@@ -0,0 +1,52 @@
1
+ ---
2
+ /**
3
+ * Functional icon set. Decorative by default (aria-hidden). Pass `label` only
4
+ * when the icon is the entire accessible name, e.g. an icon-only button.
5
+ * Icons are 24×24, stroke-based, and inherit the current text color.
6
+ */
7
+ import { ICONS } from "../data/icons";
8
+
9
+ interface Props {
10
+ name: string;
11
+ size?: number | string;
12
+ stroke?: number;
13
+ label?: string;
14
+ class?: string;
15
+ }
16
+
17
+ const { name, size = 16, stroke, label, class: className } = Astro.props;
18
+
19
+ /* Three sizes carry the whole interface: 14 (inline with small text and
20
+ inside compact controls), 16 (default), 20 (status and dialog chrome).
21
+ 24 is reserved for empty states. The stroke is derived from the size so
22
+ every icon renders at the same optical weight (about 1.2-1.5px). */
23
+ const px = typeof size === "number" ? size : Number.parseFloat(size) || 16;
24
+ const OPTICAL_STROKE: Record<number, number> = {
25
+ 14: 2,
26
+ 16: 1.75,
27
+ 20: 1.6,
28
+ 24: 1.5,
29
+ };
30
+ const strokeWidth = stroke ?? OPTICAL_STROKE[px] ?? (px <= 14 ? 2 : 1.6);
31
+
32
+ const paths = ICONS[name];
33
+ if (!paths) {
34
+ throw new Error(`Icon: unknown name "${name}".`);
35
+ }
36
+ ---
37
+
38
+ <svg
39
+ class:list={["ds-icon", className]}
40
+ width={size}
41
+ height={size}
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ stroke-width={strokeWidth}
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ role={label ? "img" : undefined}
49
+ aria-label={label}
50
+ aria-hidden={label ? undefined : "true"}
51
+ set:html={paths}
52
+ />
@@ -0,0 +1,151 @@
1
+ ---
2
+ import Button from "./Button.astro";
3
+ import MenuItem from "./MenuItem.astro";
4
+ import "../styles/popover.css";
5
+ import "../styles/menu.css";
6
+
7
+ /**
8
+ * Menu: a trigger button and a list of commands in a native popover. Opens
9
+ * with Enter, Space or the arrow keys; arrows, Home, End and typing move
10
+ * between items; Escape closes and returns focus to the trigger; Tab closes.
11
+ * Picking an item fires `ds-menu-select` on the root with `detail.value`
12
+ * (and `detail.checked` for checkbox and radio items), then closes.
13
+ *
14
+ * <Menu triggerLabel="Actions" items={[
15
+ * { value: "restart", label: "Restart" },
16
+ * { type: "separator" },
17
+ * { value: "delete", label: "Delete", danger: true },
18
+ * ]} />
19
+ */
20
+ interface Item {
21
+ value: string;
22
+ label: string;
23
+ icon?: string;
24
+ /** Renders a link instead of a button. */
25
+ href?: string;
26
+ disabled?: boolean;
27
+ danger?: boolean;
28
+ }
29
+
30
+ interface CheckItem {
31
+ type: "checkbox";
32
+ value: string;
33
+ label: string;
34
+ checked?: boolean;
35
+ disabled?: boolean;
36
+ }
37
+
38
+ interface RadioItem {
39
+ type: "radio";
40
+ value: string;
41
+ label: string;
42
+ checked?: boolean;
43
+ disabled?: boolean;
44
+ }
45
+
46
+ interface Separator {
47
+ type: "separator";
48
+ }
49
+
50
+ interface Group {
51
+ type: "group";
52
+ label: string;
53
+ items: (Item | CheckItem | RadioItem)[];
54
+ }
55
+
56
+ type Entry = Item | CheckItem | RadioItem | Separator | Group;
57
+
58
+ interface Props {
59
+ items: Entry[];
60
+ /** Trigger text, or its accessible name when `triggerIcon` is set. */
61
+ triggerLabel?: string;
62
+ /** Icon-only trigger, for example "more-horizontal". */
63
+ triggerIcon?: string;
64
+ triggerVariant?: "primary" | "secondary" | "ghost" | "danger";
65
+ placement?: "top" | "bottom" | "start" | "end";
66
+ id?: string;
67
+ class?: string;
68
+ }
69
+
70
+ const {
71
+ items,
72
+ triggerLabel = "Actions",
73
+ triggerIcon,
74
+ triggerVariant = "secondary",
75
+ placement = "bottom",
76
+ id,
77
+ class: className,
78
+ } = Astro.props;
79
+
80
+ const rootId = id ?? `menu-${Math.random().toString(36).slice(2, 9)}`;
81
+ const surfaceId = `${rootId}-surface`;
82
+ const triggerId = `${rootId}-trigger`;
83
+
84
+ const isGroup = (entry: Entry): entry is Group =>
85
+ "type" in entry && entry.type === "group";
86
+ const isSeparator = (entry: Entry): entry is Separator =>
87
+ "type" in entry && entry.type === "separator";
88
+ ---
89
+
90
+ <span class="ds-anchor" id={rootId} data-ds-menu>
91
+ <Button
92
+ id={triggerId}
93
+ variant={triggerVariant}
94
+ type="button"
95
+ popovertarget={surfaceId}
96
+ aria-haspopup="menu"
97
+ aria-expanded="false"
98
+ aria-controls={surfaceId}
99
+ iconOnly={Boolean(triggerIcon)}
100
+ ariaLabel={triggerIcon ? triggerLabel : undefined}
101
+ icon={triggerIcon ?? "chevron-down"}
102
+ iconPosition="end"
103
+ >
104
+ {!triggerIcon && triggerLabel}
105
+ </Button>
106
+ <div
107
+ class:list={[
108
+ "ds-popover",
109
+ "ds-menu",
110
+ `ds-popover--${placement}`,
111
+ className,
112
+ ]}
113
+ id={surfaceId}
114
+ popover="auto"
115
+ role="menu"
116
+ tabindex="-1"
117
+ aria-labelledby={triggerId}
118
+ >
119
+ {items.map((entry, index) =>
120
+ isSeparator(entry) ? (
121
+ <div class="ds-menu__separator" role="separator" />
122
+ ) : isGroup(entry) ? (
123
+ <div role="group" aria-labelledby={`${rootId}-group-${index}`}>
124
+ <div class="ds-menu__label" id={`${rootId}-group-${index}`}>
125
+ {entry.label}
126
+ </div>
127
+ {entry.items.map((item) => (
128
+ <MenuItem {...item} />
129
+ ))}
130
+ </div>
131
+ ) : (
132
+ <MenuItem {...entry} />
133
+ ),
134
+ )}
135
+ </div>
136
+ </span>
137
+
138
+ <script>
139
+ import { mountMenu } from "../scripts/menu";
140
+
141
+ const bind = () => {
142
+ document.querySelectorAll<HTMLElement>("[data-ds-menu]").forEach((root) => {
143
+ if (root.dataset.dsBound) return;
144
+ root.dataset.dsBound = "true";
145
+ mountMenu(root);
146
+ });
147
+ };
148
+
149
+ bind();
150
+ document.addEventListener("astro:page-load", bind);
151
+ </script>
@@ -0,0 +1,57 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+
4
+ /** One row of a Menu. Internal: use Menu with an `items` array. */
5
+ interface Props {
6
+ type?: "item" | "checkbox" | "radio";
7
+ value: string;
8
+ label: string;
9
+ icon?: string;
10
+ href?: string;
11
+ checked?: boolean;
12
+ disabled?: boolean;
13
+ danger?: boolean;
14
+ }
15
+
16
+ const {
17
+ type = "item",
18
+ value,
19
+ label,
20
+ icon,
21
+ href,
22
+ checked = false,
23
+ disabled = false,
24
+ danger = false,
25
+ } = Astro.props;
26
+
27
+ const role =
28
+ type === "checkbox"
29
+ ? "menuitemcheckbox"
30
+ : type === "radio"
31
+ ? "menuitemradio"
32
+ : "menuitem";
33
+ const Tag = href && type === "item" ? "a" : "button";
34
+ ---
35
+
36
+ <Tag
37
+ class:list={["ds-menu__item", danger && "ds-menu__item--danger"]}
38
+ role={role}
39
+ tabindex="-1"
40
+ type={Tag === "button" ? "button" : undefined}
41
+ href={Tag === "a" && !disabled ? href : undefined}
42
+ aria-checked={type === "item" ? undefined : String(checked)}
43
+ aria-disabled={disabled ? "true" : undefined}
44
+ data-value={value}
45
+ >
46
+ {type !== "item" && (
47
+ <span class="ds-menu__indicator" aria-hidden="true">
48
+ <Icon name="check" size={16} />
49
+ </span>
50
+ )}
51
+ {icon && (
52
+ <span class="ds-menu__icon" aria-hidden="true">
53
+ <Icon name={icon} size={16} />
54
+ </span>
55
+ )}
56
+ {label}
57
+ </Tag>