@sparklingopinions/sparkling-ui 0.4.1 → 0.5.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Board.js","names":["_sfc_main","Board_vue_vue_type_script_setup_true_lang_default","Board_default"],"sources":["../../../src/components/Board/Board.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends Record<string, any>\">\nimport { ref, useId } from 'vue'\n\nimport Icon from '../Icon/Icon.vue'\nimport Skeleton from '../Skeleton/Skeleton.vue'\nimport type { BoardColumn } from './types'\n\nexport interface BoardProps<T> {\n columns: BoardColumn<T>[]\n /** The item field that identifies it. */\n rowKey?: keyof T & string\n /** Cards also reorder within a column, for records that carry a position. */\n sortable?: boolean\n /** Columns run to the bottom of the parent and scroll inside it; give the parent a height. False sizes them to their cards. */\n fill?: boolean\n loading?: boolean\n /** The columns' heading level, to fit the page's outline. */\n headingLevel?: 2 | 3 | 4 | 5 | 6\n /** Adds a dashed \"New\" button at the foot of each column, shown on hover (`@new`). */\n onNew?: (column: BoardColumn<T>) => void\n newLabel?: string\n emptyLabel?: string\n}\n\nconst props = withDefaults(defineProps<BoardProps<T>>(), {\n rowKey: 'id' as never,\n fill: true,\n headingLevel: 3,\n onNew: undefined,\n newLabel: 'New',\n emptyLabel: 'No cards'\n})\n\nconst emit = defineEmits<{\n 'card-click': [item: T]\n /** A card dropped into a column, at its new index there. Show it moved at once; put it back if saving fails. */\n move: [id: unknown, column: string, index: number]\n}>()\n\ndefineSlots<{\n /** A card's content; `item.name` as a title by default. */\n card?: (props: { item: T; column: BoardColumn<T> }) => unknown\n /** Replaces a column's count, e.g. its summed deal value. */\n count?: (props: { column: BoardColumn<T> }) => unknown\n}>()\n\nconst id = useId()\nconst idOf = (item: T): unknown => item[props.rowKey ?? 'id']\n\nconst dragged = ref<{ id: unknown; column: string; index: number } | null>(null)\nconst target = ref<{ column: string; index: number } | null>(null)\n\nfunction onDragStart(event: DragEvent, item: T, column: string, index: number) {\n if (event.dataTransfer) {\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', String(idOf(item)))\n }\n dragged.value = { id: idOf(item), column, index }\n}\n\nfunction onDragEnd() {\n dragged.value = null\n target.value = null\n}\n\nfunction overColumn(event: DragEvent, column: BoardColumn<T>) {\n if (!dragged.value) return\n event.preventDefault()\n if (target.value?.column !== column.id) target.value = { column: column.id, index: column.items.length }\n}\n\n// The card's top half drops before it, the bottom half after.\nfunction overCard(event: DragEvent, column: string, index: number) {\n if (!dragged.value) return\n event.preventDefault()\n event.stopPropagation()\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const at = event.clientY < rect.top + rect.height / 2 ? index : index + 1\n if (target.value?.column !== column || target.value.index !== at) target.value = { column, index: at }\n}\n\nfunction onLeave(event: DragEvent) {\n if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node | null)) target.value = null\n}\n\nfunction onDrop(event: DragEvent, column: BoardColumn<T>) {\n event.preventDefault()\n const from = dragged.value\n if (from) {\n let index = target.value?.column === column.id ? target.value.index : column.items.length\n if (from.column === column.id) {\n // Within a column the dragged card leaves its old slot first.\n if (index > from.index) index--\n if (props.sortable && index !== from.index) emit('move', from.id, column.id, index)\n } else {\n emit('move', from.id, column.id, index)\n }\n }\n onDragEnd()\n}\n\nfunction onCardKeydown(event: KeyboardEvent, item: T) {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return\n event.preventDefault()\n emit('card-click', item)\n}\n\nconst isTarget = (column: string, index: number, item: T) =>\n target.value?.column === column && target.value.index === index && dragged.value?.id !== idOf(item)\n</script>\n\n<!-- A board: cards in columns by a field (a stage, a status), dragged between columns. -->\n<template>\n <div\n class=\"flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']\"\n :class=\"fill && 'h-full min-h-0 flex-1'\"\n >\n <section\n v-for=\"column in columns\"\n :key=\"column.id\"\n :aria-labelledby=\"`${id}-${column.id}`\"\n class=\"flex min-h-0 w-full max-w-xs min-w-[16rem] shrink-0 flex-col\"\n >\n <header class=\"flex items-center justify-between px-1 pb-2.5\">\n <component\n :is=\"`h${headingLevel}`\"\n :id=\"`${id}-${column.id}`\"\n class=\"m-0 flex items-center gap-2 text-sm font-semibold text-sui-ink\"\n >\n <Icon v-if=\"column.icon\" :color=\"column.iconColor ?? 'ink-subtle'\" :name=\"column.icon\" />\n {{ column.name }}\n </component>\n <span\n class=\"ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums\"\n >\n <slot :column=\"column\" name=\"count\">{{ column.items.length }}</slot>\n </span>\n </header>\n <div\n class=\"group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150\"\n :class=\"target?.column === column.id && 'ring-2 ring-sui-accent-line ring-inset'\"\n @dragleave=\"onLeave\"\n @dragover=\"overColumn($event, column)\"\n @drop=\"onDrop($event, column)\"\n >\n <template v-if=\"loading\">\n <div\n v-for=\"i in 3\"\n :key=\"i\"\n aria-hidden=\"true\"\n class=\"rounded-xl border border-sui-line-subtle bg-sui-surface p-4\"\n >\n <Skeleton class=\"mb-3\" />\n <Skeleton height=\"0.75rem\" width=\"70%\" />\n </div>\n </template>\n <p\n v-else-if=\"column.items.length === 0 && !dragged\"\n class=\"m-0 px-1 py-3 text-center text-13 text-sui-ink-muted\"\n >\n {{ emptyLabel }}\n </p>\n <ul v-else class=\"m-0 flex list-none flex-col gap-2.5 p-0\">\n <li v-for=\"(item, index) in column.items\" :key=\"idOf(item) as PropertyKey\">\n <div\n class=\"relative cursor-pointer rounded-xl border border-sui-line-subtle bg-sui-surface p-4 outline-hidden transition-[border-color,opacity] duration-150 hover:border-sui-line focus-visible:sui-ring\"\n :class=\"[\n dragged?.id === idOf(item) && 'opacity-40',\n isTarget(column.id, index, item) && 'shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]'\n ]\"\n draggable=\"true\"\n role=\"button\"\n tabindex=\"0\"\n @click=\"emit('card-click', item)\"\n @dragend=\"onDragEnd\"\n @dragover=\"overCard($event, column.id, index)\"\n @dragstart=\"onDragStart($event, item, column.id, index)\"\n @keydown=\"onCardKeydown($event, item)\"\n >\n <slot :column=\"column\" :item=\"item\" name=\"card\">\n <p class=\"m-0 text-sm font-semibold text-sui-ink\">{{ item.name }}</p>\n </slot>\n </div>\n </li>\n </ul>\n <button\n v-if=\"onNew && !loading\"\n class=\"flex h-9 w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-dashed border-sui-line-strong text-xs font-medium text-sui-ink-muted opacity-0 outline-hidden transition-all duration-150 group-hover/body:opacity-100 hover:border-sui-ink-muted hover:bg-sui-surface/60 hover:text-sui-ink-body focus-visible:opacity-100 focus-visible:sui-ring\"\n type=\"button\"\n @click=\"onNew(column)\"\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 14 14\"\n >\n <path d=\"M7 2.5v9M2.5 7h9\" />\n </svg>\n {{ newLabel }}\n </button>\n </div>\n </section>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"Board.js","names":["_sfc_main","Board_vue_vue_type_script_setup_true_lang_default","Board_default"],"sources":["../../../src/components/Board/Board.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends Record<string, any>\">\nimport { onBeforeUnmount, ref, useId } from 'vue'\n\nimport Icon from '../Icon/Icon.vue'\nimport Skeleton from '../Skeleton/Skeleton.vue'\nimport type { BoardColumn } from './types'\n\nexport interface BoardProps<T> {\n columns: BoardColumn<T>[]\n /** The item field that identifies it. */\n rowKey?: keyof T & string\n /** Cards also reorder within a column, for records that carry a position. */\n sortable?: boolean\n /** Columns run to the bottom of the parent and scroll inside it; give the parent a height. False sizes them to their cards. */\n fill?: boolean\n loading?: boolean\n /** The columns' heading level, to fit the page's outline. */\n headingLevel?: 2 | 3 | 4 | 5 | 6\n /** Draws each card's border, padding and surface. False when the `card` slot draws its own. */\n cardFrame?: boolean\n /** Makes a card a button that opens on click, Enter and Space (`@card-click`). Without it, the card slot carries its\n * own link or buttons. */\n onCardClick?: (item: T) => void\n /** Adds a dashed \"New\" button at the foot of each column, shown on hover (`@new`). */\n onNew?: (column: BoardColumn<T>) => void\n newLabel?: string\n emptyLabel?: string\n}\n\nconst props = withDefaults(defineProps<BoardProps<T>>(), {\n rowKey: 'id' as never,\n fill: true,\n headingLevel: 3,\n cardFrame: true,\n onCardClick: undefined,\n onNew: undefined,\n newLabel: 'New',\n emptyLabel: 'No cards'\n})\n\nconst emit = defineEmits<{\n /** A card dropped into a column, at its new index there. Show it moved at once; put it back if saving fails. */\n move: [id: unknown, column: string, index: number]\n}>()\n\ndefineSlots<{\n /** A card's content; `item.name` as a title by default. */\n card?: (props: { item: T; column: BoardColumn<T> }) => unknown\n /** Replaces a column's count, e.g. its summed deal value. */\n count?: (props: { column: BoardColumn<T> }) => unknown\n}>()\n\nconst id = useId()\nconst idOf = (item: T): unknown => item[props.rowKey ?? 'id']\nconst root = ref<HTMLElement>()\n\nconst dragged = ref<{ id: unknown; column: string; index: number } | null>(null)\nconst target = ref<{ column: string; index: number } | null>(null)\n\n// Dragging runs on pointer events, so it works with a mouse, a pen and a finger. A mouse drags once it moves a few\n// pixels, so a click still clicks; a finger after a short press, so a swipe still scrolls.\nconst MOUSE_SLOP = 4\nconst TOUCH_SLOP = 8\nconst TOUCH_DELAY = 250\nconst EDGE = 48\n\nlet pending: {\n item: T\n column: string\n index: number\n x: number\n y: number\n card: HTMLElement\n touch: boolean\n} | null = null\nlet pressTimer: number | undefined\nlet ghost: HTMLElement | null = null\nlet grab = { x: 0, y: 0 }\nlet justDragged = false\n\nfunction onPointerDown(event: PointerEvent, item: T, column: string, index: number) {\n if (event.button !== 0 || (event.target as HTMLElement).closest('input, textarea, select, [contenteditable]')) return\n const touch = event.pointerType === 'touch'\n pending = { item, column, index, x: event.clientX, y: event.clientY, card: event.currentTarget as HTMLElement, touch }\n if (touch) pressTimer = window.setTimeout(() => pending && start(), TOUCH_DELAY)\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', stop)\n window.addEventListener('touchmove', holdScroll, { passive: false })\n window.addEventListener('keydown', onEscape)\n}\n\nfunction start() {\n if (!pending) return\n const rect = pending.card.getBoundingClientRect()\n grab = { x: pending.x - rect.left, y: pending.y - rect.top }\n ghost = pending.card.cloneNode(true) as HTMLElement\n ghost.removeAttribute('id')\n ghost.setAttribute('aria-hidden', 'true')\n ghost.classList.add('pointer-events-none', 'fixed', 'top-0', 'left-0', 'z-[1300]', 'shadow-lg', 'opacity-95')\n ghost.style.width = `${rect.width}px`\n document.body.appendChild(ghost)\n follow(pending.x, pending.y)\n dragged.value = { id: idOf(pending.item), column: pending.column, index: pending.index }\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!pending) return\n if (!dragged.value) {\n const moved = Math.hypot(event.clientX - pending.x, event.clientY - pending.y)\n // A finger that moves before the press is long enough is scrolling.\n if (pending.touch) return moved > TOUCH_SLOP ? stop() : undefined\n if (moved < MOUSE_SLOP) return\n start()\n }\n event.preventDefault()\n follow(event.clientX, event.clientY)\n aim(event.clientX, event.clientY)\n scrollNear(event.clientX, event.clientY)\n}\n\nfunction follow(x: number, y: number) {\n if (ghost) ghost.style.transform = `translate(${x - grab.x}px, ${y - grab.y}px) rotate(1.5deg)`\n}\n\n// What's under the pointer: a card's top half drops before it, the bottom half after; an empty spot, at the end.\nfunction aim(x: number, y: number) {\n const under = document.elementFromPoint(x, y)\n const body = under?.closest<HTMLElement>('[data-board-column]')\n if (!body || !root.value?.contains(body)) {\n target.value = null\n return\n }\n const column = body.dataset.boardColumn!\n const card = under!.closest<HTMLElement>('[data-board-index]')\n let index = props.columns.find((c) => c.id === column)?.items.length ?? 0\n if (card && body.contains(card)) {\n const rect = card.getBoundingClientRect()\n index = Number(card.dataset.boardIndex) + (y < rect.top + rect.height / 2 ? 0 : 1)\n }\n if (target.value?.column !== column || target.value.index !== index) target.value = { column, index }\n}\n\n// Near a column's top or bottom, or the board's sides, it scrolls that way.\nfunction scrollNear(x: number, y: number) {\n const body = document.elementFromPoint(x, y)?.closest<HTMLElement>('[data-board-column]')\n if (body) {\n const rect = body.getBoundingClientRect()\n if (y < rect.top + EDGE) body.scrollTop -= 12\n else if (y > rect.bottom - EDGE) body.scrollTop += 12\n }\n const board = root.value?.getBoundingClientRect()\n if (board && root.value) {\n if (x < board.left + EDGE) root.value.scrollLeft -= 12\n else if (x > board.right - EDGE) root.value.scrollLeft += 12\n }\n}\n\nfunction onPointerUp() {\n const from = dragged.value\n const to = target.value\n if (from && to) {\n let index = to.index\n if (from.column === to.column) {\n // Within a column the dragged card leaves its old slot first.\n if (index > from.index) index--\n if (props.sortable && index !== from.index) emit('move', from.id, to.column, index)\n } else {\n emit('move', from.id, to.column, index)\n }\n }\n stop()\n}\n\nfunction holdScroll(event: TouchEvent) {\n if (dragged.value) event.preventDefault()\n}\n\nfunction onEscape(event: KeyboardEvent) {\n if (event.key === 'Escape' && dragged.value) {\n event.preventDefault()\n stop()\n }\n}\n\nfunction stop() {\n window.clearTimeout(pressTimer)\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', stop)\n window.removeEventListener('touchmove', holdScroll)\n window.removeEventListener('keydown', onEscape)\n ghost?.remove()\n ghost = null\n // The click that ends a drag isn't a click on the card.\n if (dragged.value) {\n justDragged = true\n window.setTimeout(() => (justDragged = false))\n }\n pending = null\n dragged.value = null\n target.value = null\n}\n\nonBeforeUnmount(stop)\n\nfunction clickCard(event: MouseEvent, item: T) {\n if (justDragged) {\n event.preventDefault()\n event.stopPropagation()\n return\n }\n props.onCardClick?.(item)\n}\n\nfunction onCardKeydown(event: KeyboardEvent, item: T) {\n if (!props.onCardClick || event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return\n event.preventDefault()\n props.onCardClick(item)\n}\n\nconst isTarget = (column: string, index: number, item: T) =>\n target.value?.column === column && target.value.index === index && dragged.value?.id !== idOf(item)\n\nconst frame =\n 'border border-sui-line-subtle bg-sui-surface p-4 transition-[border-color,opacity] duration-150 hover:border-sui-line'\n</script>\n\n<!-- A board: cards in columns by a field (a stage, a status), dragged between columns. -->\n<template>\n <div\n ref=\"root\"\n class=\"flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']\"\n :class=\"fill && 'h-full min-h-0 flex-1'\"\n >\n <section\n v-for=\"column in columns\"\n :key=\"column.id\"\n :aria-labelledby=\"`${id}-${column.id}`\"\n class=\"flex min-h-0 w-full max-w-xs min-w-[16rem] shrink-0 flex-col\"\n >\n <header class=\"flex items-center justify-between px-1 pb-2.5\">\n <component\n :is=\"`h${headingLevel}`\"\n :id=\"`${id}-${column.id}`\"\n class=\"m-0 flex items-center gap-2 text-sm font-semibold text-sui-ink\"\n >\n <Icon v-if=\"column.icon\" :color=\"column.iconColor ?? 'ink-subtle'\" :name=\"column.icon\" />\n {{ column.name }}\n </component>\n <span\n class=\"ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums\"\n >\n <slot :column=\"column\" name=\"count\">{{ column.items.length }}</slot>\n </span>\n </header>\n <div\n class=\"group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150\"\n :class=\"target?.column === column.id && 'ring-2 ring-sui-accent-line ring-inset'\"\n :data-board-column=\"column.id\"\n >\n <template v-if=\"loading\">\n <div\n v-for=\"i in 3\"\n :key=\"i\"\n aria-hidden=\"true\"\n class=\"rounded-xl border border-sui-line-subtle bg-sui-surface p-4\"\n >\n <Skeleton class=\"mb-3\" />\n <Skeleton height=\"0.75rem\" width=\"70%\" />\n </div>\n </template>\n <p\n v-else-if=\"column.items.length === 0 && !dragged\"\n class=\"m-0 px-1 py-3 text-center text-13 text-sui-ink-muted\"\n >\n {{ emptyLabel }}\n </p>\n <ul v-else class=\"m-0 flex list-none flex-col gap-2.5 p-0\">\n <li v-for=\"(item, index) in column.items\" :key=\"idOf(item) as PropertyKey\">\n <div\n class=\"relative rounded-xl outline-hidden transition-opacity duration-150 select-none\"\n :class=\"[\n cardFrame && frame,\n onCardClick && 'cursor-pointer focus-visible:sui-ring',\n dragged?.id === idOf(item) && 'opacity-40',\n isTarget(column.id, index, item) && 'shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]'\n ]\"\n :data-board-index=\"index\"\n :role=\"onCardClick ? 'button' : undefined\"\n :tabindex=\"onCardClick ? 0 : undefined\"\n @click.capture=\"clickCard($event, item)\"\n @contextmenu=\"dragged && $event.preventDefault()\"\n @dragstart.prevent\n @keydown=\"onCardKeydown($event, item)\"\n @pointerdown=\"onPointerDown($event, item, column.id, index)\"\n >\n <slot :column=\"column\" :item=\"item\" name=\"card\">\n <p class=\"m-0 text-sm font-semibold text-sui-ink\">{{ item.name }}</p>\n </slot>\n </div>\n </li>\n </ul>\n <button\n v-if=\"onNew && !loading\"\n class=\"flex h-9 w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-dashed border-sui-line-strong text-xs font-medium text-sui-ink-muted opacity-0 outline-hidden transition-all duration-150 group-hover/body:opacity-100 hover:border-sui-ink-muted hover:bg-sui-surface/60 hover:text-sui-ink-body focus-visible:opacity-100 focus-visible:sui-ring\"\n type=\"button\"\n @click=\"onNew(column)\"\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 14 14\"\n >\n <path d=\"M7 2.5v9M2.5 7h9\" />\n </svg>\n {{ newLabel }}\n </button>\n </div>\n </section>\n </div>\n</template>\n"],"mappings":""}
@@ -10,6 +10,11 @@ export interface BoardProps<T> {
10
10
  loading?: boolean;
11
11
  /** The columns' heading level, to fit the page's outline. */
12
12
  headingLevel?: 2 | 3 | 4 | 5 | 6;
13
+ /** Draws each card's border, padding and surface. False when the `card` slot draws its own. */
14
+ cardFrame?: boolean;
15
+ /** Makes a card a button that opens on click, Enter and Space (`@card-click`). Without it, the card slot carries its
16
+ * own link or buttons. */
17
+ onCardClick?: (item: T) => void;
13
18
  /** Adds a dashed "New" button at the foot of each column, shown on hover (`@new`). */
14
19
  onNew?: (column: BoardColumn<T>) => void;
15
20
  newLabel?: string;
@@ -17,7 +22,6 @@ export interface BoardProps<T> {
17
22
  }
18
23
  declare const __VLS_export: <T extends Record<string, any>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
19
24
  props: import("vue").PublicProps & __VLS_PrettifyLocal<BoardProps<T> & {
20
- "onCard-click"?: ((item: T) => any) | undefined;
21
25
  onMove?: ((id: unknown, column: string, index: number) => any) | undefined;
22
26
  }> & (typeof globalThis extends {
23
27
  __VLS_PROPS_FALLBACK: infer P;
@@ -35,7 +39,7 @@ declare const __VLS_export: <T extends Record<string, any>>(__VLS_props: NonNull
35
39
  column: BoardColumn<T>;
36
40
  }) => unknown;
37
41
  };
38
- emit: ((evt: "card-click", item: T) => void) & ((evt: "move", id: unknown, column: string, index: number) => void);
42
+ emit: (evt: "move", id: unknown, column: string, index: number) => void;
39
43
  }>) => import("vue").VNode & {
40
44
  __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
41
45
  };
@@ -1,19 +1,21 @@
1
1
  import e from "../Icon/Icon.js";
2
2
  import t from "../Skeleton/Skeleton.js";
3
- import { Fragment as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createElementVNode as o, createTextVNode as s, createVNode as c, defineComponent as l, normalizeClass as u, openBlock as d, ref as f, renderList as p, renderSlot as m, resolveDynamicComponent as h, toDisplayString as g, unref as _, useId as v, withCtx as y } from "vue";
3
+ import { Fragment as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createElementVNode as o, createTextVNode as s, createVNode as c, defineComponent as l, normalizeClass as u, onBeforeUnmount as d, openBlock as f, ref as p, renderList as m, renderSlot as h, resolveDynamicComponent as g, toDisplayString as _, unref as v, useId as y, withCtx as b, withModifiers as ee } from "vue";
4
4
  //#region src/components/Board/Board.vue?vue&type=script&setup=true&lang.ts
5
- var b = ["aria-labelledby"], x = { class: "flex items-center justify-between px-1 pb-2.5" }, S = { class: "ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums" }, C = ["onDragover", "onDrop"], w = {
5
+ var te = ["aria-labelledby"], ne = { class: "flex items-center justify-between px-1 pb-2.5" }, re = { class: "ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums" }, x = ["data-board-column"], S = {
6
6
  key: 1,
7
7
  class: "m-0 px-1 py-3 text-center text-13 text-sui-ink-muted"
8
- }, T = {
8
+ }, C = {
9
9
  key: 2,
10
10
  class: "m-0 flex list-none flex-col gap-2.5 p-0"
11
- }, E = [
12
- "onClick",
13
- "onDragover",
14
- "onDragstart",
15
- "onKeydown"
16
- ], D = { class: "m-0 text-sm font-semibold text-sui-ink" }, O = ["onClick"], k = /*@__PURE__*/ l({
11
+ }, w = [
12
+ "data-board-index",
13
+ "role",
14
+ "tabindex",
15
+ "onClickCapture",
16
+ "onKeydown",
17
+ "onPointerdown"
18
+ ], T = { class: "m-0 text-sm font-semibold text-sui-ink" }, E = ["onClick"], D = 4, O = 8, k = 250, A = 48, j = "border border-sui-line-subtle bg-sui-surface p-4 transition-[border-color,opacity] duration-150 hover:border-sui-line", M = /*@__PURE__*/ l({
17
19
  __name: "Board",
18
20
  props: {
19
21
  columns: {},
@@ -25,6 +27,14 @@ var b = ["aria-labelledby"], x = { class: "flex items-center justify-between px-
25
27
  },
26
28
  loading: { type: Boolean },
27
29
  headingLevel: { default: 3 },
30
+ cardFrame: {
31
+ type: Boolean,
32
+ default: !0
33
+ },
34
+ onCardClick: {
35
+ type: Function,
36
+ default: void 0
37
+ },
28
38
  onNew: {
29
39
  type: Function,
30
40
  default: void 0
@@ -32,104 +42,162 @@ var b = ["aria-labelledby"], x = { class: "flex items-center justify-between px-
32
42
  newLabel: { default: "New" },
33
43
  emptyLabel: { default: "No cards" }
34
44
  },
35
- emits: ["card-click", "move"],
36
- setup(l, { emit: k }) {
37
- let A = l, j = k, M = v(), N = (e) => e[A.rowKey ?? "id"], P = f(null), F = f(null);
38
- function I(e, t, n, r) {
39
- e.dataTransfer && (e.dataTransfer.effectAllowed = "move", e.dataTransfer.setData("text/plain", String(N(t)))), P.value = {
40
- id: N(t),
45
+ emits: ["move"],
46
+ setup(l, { emit: M }) {
47
+ let N = l, P = M, F = y(), I = (e) => e[N.rowKey ?? "id"], L = p(), R = p(null), z = p(null), B = null, V, H = null, U = {
48
+ x: 0,
49
+ y: 0
50
+ }, W = !1;
51
+ function G(e, t, n, r) {
52
+ if (e.button !== 0 || e.target.closest("input, textarea, select, [contenteditable]")) return;
53
+ let i = e.pointerType === "touch";
54
+ B = {
55
+ item: t,
41
56
  column: n,
42
- index: r
57
+ index: r,
58
+ x: e.clientX,
59
+ y: e.clientY,
60
+ card: e.currentTarget,
61
+ touch: i
62
+ }, i && (V = window.setTimeout(() => B && K(), k)), window.addEventListener("pointermove", q), window.addEventListener("pointerup", X), window.addEventListener("pointercancel", $), window.addEventListener("touchmove", Z, { passive: !1 }), window.addEventListener("keydown", Q);
63
+ }
64
+ function K() {
65
+ if (!B) return;
66
+ let e = B.card.getBoundingClientRect();
67
+ U = {
68
+ x: B.x - e.left,
69
+ y: B.y - e.top
70
+ }, H = B.card.cloneNode(!0), H.removeAttribute("id"), H.setAttribute("aria-hidden", "true"), H.classList.add("pointer-events-none", "fixed", "top-0", "left-0", "z-[1300]", "shadow-lg", "opacity-95"), H.style.width = `${e.width}px`, document.body.appendChild(H), J(B.x, B.y), R.value = {
71
+ id: I(B.item),
72
+ column: B.column,
73
+ index: B.index
43
74
  };
44
75
  }
45
- function L() {
46
- P.value = null, F.value = null;
76
+ function q(e) {
77
+ if (B) {
78
+ if (!R.value) {
79
+ let t = Math.hypot(e.clientX - B.x, e.clientY - B.y);
80
+ if (B.touch) return t > O ? $() : void 0;
81
+ if (t < D) return;
82
+ K();
83
+ }
84
+ e.preventDefault(), J(e.clientX, e.clientY), Y(e.clientX, e.clientY), ie(e.clientX, e.clientY);
85
+ }
47
86
  }
48
- function R(e, t) {
49
- P.value && (e.preventDefault(), F.value?.column !== t.id && (F.value = {
50
- column: t.id,
51
- index: t.items.length
52
- }));
87
+ function J(e, t) {
88
+ H && (H.style.transform = `translate(${e - U.x}px, ${t - U.y}px) rotate(1.5deg)`);
53
89
  }
54
- function z(e, t, n) {
55
- if (!P.value) return;
56
- e.preventDefault(), e.stopPropagation();
57
- let r = e.currentTarget.getBoundingClientRect(), i = e.clientY < r.top + r.height / 2 ? n : n + 1;
58
- (F.value?.column !== t || F.value.index !== i) && (F.value = {
59
- column: t,
60
- index: i
90
+ function Y(e, t) {
91
+ let n = document.elementFromPoint(e, t), r = n?.closest("[data-board-column]");
92
+ if (!r || !L.value?.contains(r)) {
93
+ z.value = null;
94
+ return;
95
+ }
96
+ let i = r.dataset.boardColumn, a = n.closest("[data-board-index]"), o = N.columns.find((e) => e.id === i)?.items.length ?? 0;
97
+ if (a && r.contains(a)) {
98
+ let e = a.getBoundingClientRect();
99
+ o = Number(a.dataset.boardIndex) + (t < e.top + e.height / 2 ? 0 : 1);
100
+ }
101
+ (z.value?.column !== i || z.value.index !== o) && (z.value = {
102
+ column: i,
103
+ index: o
61
104
  });
62
105
  }
63
- function B(e) {
64
- e.currentTarget.contains(e.relatedTarget) || (F.value = null);
65
- }
66
- function V(e, t) {
67
- e.preventDefault();
68
- let n = P.value;
106
+ function ie(e, t) {
107
+ let n = document.elementFromPoint(e, t)?.closest("[data-board-column]");
69
108
  if (n) {
70
- let e = F.value?.column === t.id ? F.value.index : t.items.length;
71
- n.column === t.id ? (e > n.index && e--, A.sortable && e !== n.index && j("move", n.id, t.id, e)) : j("move", n.id, t.id, e);
109
+ let e = n.getBoundingClientRect();
110
+ t < e.top + A ? n.scrollTop -= 12 : t > e.bottom - A && (n.scrollTop += 12);
111
+ }
112
+ let r = L.value?.getBoundingClientRect();
113
+ r && L.value && (e < r.left + A ? L.value.scrollLeft -= 12 : e > r.right - A && (L.value.scrollLeft += 12));
114
+ }
115
+ function X() {
116
+ let e = R.value, t = z.value;
117
+ if (e && t) {
118
+ let n = t.index;
119
+ e.column === t.column ? (n > e.index && n--, N.sortable && n !== e.index && P("move", e.id, t.column, n)) : P("move", e.id, t.column, n);
72
120
  }
73
- L();
121
+ $();
122
+ }
123
+ function Z(e) {
124
+ R.value && e.preventDefault();
125
+ }
126
+ function Q(e) {
127
+ e.key === "Escape" && R.value && (e.preventDefault(), $());
128
+ }
129
+ function $() {
130
+ window.clearTimeout(V), window.removeEventListener("pointermove", q), window.removeEventListener("pointerup", X), window.removeEventListener("pointercancel", $), window.removeEventListener("touchmove", Z), window.removeEventListener("keydown", Q), H?.remove(), H = null, R.value && (W = !0, window.setTimeout(() => W = !1)), B = null, R.value = null, z.value = null;
131
+ }
132
+ d($);
133
+ function ae(e, t) {
134
+ W ? (e.preventDefault(), e.stopPropagation()) : N.onCardClick?.(t);
74
135
  }
75
- function H(e, t) {
76
- e.target !== e.currentTarget || e.key !== "Enter" && e.key !== " " || (e.preventDefault(), j("card-click", t));
136
+ function oe(e, t) {
137
+ !N.onCardClick || e.target !== e.currentTarget || e.key !== "Enter" && e.key !== " " || (e.preventDefault(), N.onCardClick(t));
77
138
  }
78
- let U = (e, t, n) => F.value?.column === e && F.value.index === t && P.value?.id !== N(n);
79
- return (f, v) => (d(), a("div", { class: u(["flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']", l.fill && "h-full min-h-0 flex-1"]) }, [(d(!0), a(n, null, p(l.columns, (k) => (d(), a("section", {
80
- key: k.id,
81
- "aria-labelledby": `${_(M)}-${k.id}`,
139
+ let se = (e, t, n) => z.value?.column === e && z.value.index === t && R.value?.id !== I(n);
140
+ return (d, p) => (f(), a("div", {
141
+ ref_key: "root",
142
+ ref: L,
143
+ class: u(["flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']", l.fill && "h-full min-h-0 flex-1"])
144
+ }, [(f(!0), a(n, null, m(l.columns, (y) => (f(), a("section", {
145
+ key: y.id,
146
+ "aria-labelledby": `${v(F)}-${y.id}`,
82
147
  class: "flex min-h-0 w-full max-w-xs min-w-[16rem] shrink-0 flex-col"
83
- }, [o("header", x, [(d(), r(h(`h${l.headingLevel}`), {
84
- id: `${_(M)}-${k.id}`,
148
+ }, [o("header", ne, [(f(), r(g(`h${l.headingLevel}`), {
149
+ id: `${v(F)}-${y.id}`,
85
150
  class: "m-0 flex items-center gap-2 text-sm font-semibold text-sui-ink"
86
151
  }, {
87
- default: y(() => [k.icon ? (d(), r(e, {
152
+ default: b(() => [y.icon ? (f(), r(e, {
88
153
  key: 0,
89
- color: k.iconColor ?? "ink-subtle",
90
- name: k.icon
91
- }, null, 8, ["color", "name"])) : i("", !0), s(" " + g(k.name), 1)]),
154
+ color: y.iconColor ?? "ink-subtle",
155
+ name: y.icon
156
+ }, null, 8, ["color", "name"])) : i("", !0), s(" " + _(y.name), 1)]),
92
157
  _: 2
93
- }, 1032, ["id"])), o("span", S, [m(f.$slots, "count", { column: k }, () => [s(g(k.items.length), 1)])])]), o("div", {
94
- class: u(["group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150", F.value?.column === k.id && "ring-2 ring-sui-accent-line ring-inset"]),
95
- onDragleave: B,
96
- onDragover: (e) => R(e, k),
97
- onDrop: (e) => V(e, k)
98
- }, [l.loading ? (d(), a(n, { key: 0 }, p(3, (e) => o("div", {
158
+ }, 1032, ["id"])), o("span", re, [h(d.$slots, "count", { column: y }, () => [s(_(y.items.length), 1)])])]), o("div", {
159
+ class: u(["group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150", z.value?.column === y.id && "ring-2 ring-sui-accent-line ring-inset"]),
160
+ "data-board-column": y.id
161
+ }, [l.loading ? (f(), a(n, { key: 0 }, m(3, (e) => o("div", {
99
162
  key: e,
100
163
  "aria-hidden": "true",
101
164
  class: "rounded-xl border border-sui-line-subtle bg-sui-surface p-4"
102
165
  }, [c(t, { class: "mb-3" }), c(t, {
103
166
  height: "0.75rem",
104
167
  width: "70%"
105
- })])), 64)) : k.items.length === 0 && !P.value ? (d(), a("p", w, g(l.emptyLabel), 1)) : (d(), a("ul", T, [(d(!0), a(n, null, p(k.items, (e, t) => (d(), a("li", { key: N(e) }, [o("div", {
106
- class: u(["relative cursor-pointer rounded-xl border border-sui-line-subtle bg-sui-surface p-4 outline-hidden transition-[border-color,opacity] duration-150 hover:border-sui-line focus-visible:sui-ring", [P.value?.id === N(e) && "opacity-40", U(k.id, t, e) && "shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]"]]),
107
- draggable: "true",
108
- role: "button",
109
- tabindex: "0",
110
- onClick: (t) => j("card-click", e),
111
- onDragend: L,
112
- onDragover: (e) => z(e, k.id, t),
113
- onDragstart: (n) => I(n, e, k.id, t),
114
- onKeydown: (t) => H(t, e)
115
- }, [m(f.$slots, "card", {
116
- column: k,
168
+ })])), 64)) : y.items.length === 0 && !R.value ? (f(), a("p", S, _(l.emptyLabel), 1)) : (f(), a("ul", C, [(f(!0), a(n, null, m(y.items, (e, t) => (f(), a("li", { key: I(e) }, [o("div", {
169
+ class: u(["relative rounded-xl outline-hidden transition-opacity duration-150 select-none", [
170
+ l.cardFrame && j,
171
+ l.onCardClick && "cursor-pointer focus-visible:sui-ring",
172
+ R.value?.id === I(e) && "opacity-40",
173
+ se(y.id, t, e) && "shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]"
174
+ ]]),
175
+ "data-board-index": t,
176
+ role: l.onCardClick ? "button" : void 0,
177
+ tabindex: l.onCardClick ? 0 : void 0,
178
+ onClickCapture: (t) => ae(t, e),
179
+ onContextmenu: p[0] ||= (e) => R.value && e.preventDefault(),
180
+ onDragstart: p[1] ||= ee(() => {}, ["prevent"]),
181
+ onKeydown: (t) => oe(t, e),
182
+ onPointerdown: (n) => G(n, e, y.id, t)
183
+ }, [h(d.$slots, "card", {
184
+ column: y,
117
185
  item: e
118
- }, () => [o("p", D, g(e.name), 1)])], 42, E)]))), 128))])), l.onNew && !l.loading ? (d(), a("button", {
186
+ }, () => [o("p", T, _(e.name), 1)])], 42, w)]))), 128))])), l.onNew && !l.loading ? (f(), a("button", {
119
187
  key: 3,
120
188
  class: "flex h-9 w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-dashed border-sui-line-strong text-xs font-medium text-sui-ink-muted opacity-0 outline-hidden transition-all duration-150 group-hover/body:opacity-100 hover:border-sui-ink-muted hover:bg-sui-surface/60 hover:text-sui-ink-body focus-visible:opacity-100 focus-visible:sui-ring",
121
189
  type: "button",
122
- onClick: (e) => l.onNew(k)
123
- }, [v[0] ||= o("svg", {
190
+ onClick: (e) => l.onNew(y)
191
+ }, [p[2] ||= o("svg", {
124
192
  "aria-hidden": "true",
125
193
  class: "size-3.5 fill-none stroke-current",
126
194
  "stroke-linecap": "round",
127
195
  "stroke-width": "1.5",
128
196
  viewBox: "0 0 14 14"
129
- }, [o("path", { d: "M7 2.5v9M2.5 7h9" })], -1), s(" " + g(l.newLabel), 1)], 8, O)) : i("", !0)], 42, C)], 8, b))), 128))], 2));
197
+ }, [o("path", { d: "M7 2.5v9M2.5 7h9" })], -1), s(" " + _(l.newLabel), 1)], 8, E)) : i("", !0)], 10, x)], 8, te))), 128))], 2));
130
198
  }
131
199
  });
132
200
  //#endregion
133
- export { k as default };
201
+ export { M as default };
134
202
 
135
203
  //# sourceMappingURL=Board.vue_vue_type_script_setup_true_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Board.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_Fragment","_renderList","_unref","_createElementVNode","_createBlock","_resolveDynamicComponent","_withCtx","Icon","_createCommentVNode","_createTextVNode","_toDisplayString","_renderSlot","_createVNode","Skeleton","Icon_default","Skeleton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","Board_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","id","useId","idOf","item","dragged","ref","target","onDragStart","event","onDragEnd","overColumn","column","overCard","rect","at","index","onLeave","onDrop","from","onCardKeydown","isTarget","openBlock","createElementBlock","normalizeClass","Fragment","renderList","unref","createElementVNode","createBlock","resolveDynamicComponent","withCtx","createCommentVNode","createTextVNode","toDisplayString","renderSlot","_ctx","$event","i","createVNode","_cache"],"sources":["../../../src/components/Board/Board.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends Record<string, any>\">\nimport { ref, useId } from 'vue'\n\nimport Icon from '../Icon/Icon.vue'\nimport Skeleton from '../Skeleton/Skeleton.vue'\nimport type { BoardColumn } from './types'\n\nexport interface BoardProps<T> {\n columns: BoardColumn<T>[]\n /** The item field that identifies it. */\n rowKey?: keyof T & string\n /** Cards also reorder within a column, for records that carry a position. */\n sortable?: boolean\n /** Columns run to the bottom of the parent and scroll inside it; give the parent a height. False sizes them to their cards. */\n fill?: boolean\n loading?: boolean\n /** The columns' heading level, to fit the page's outline. */\n headingLevel?: 2 | 3 | 4 | 5 | 6\n /** Adds a dashed \"New\" button at the foot of each column, shown on hover (`@new`). */\n onNew?: (column: BoardColumn<T>) => void\n newLabel?: string\n emptyLabel?: string\n}\n\nconst props = withDefaults(defineProps<BoardProps<T>>(), {\n rowKey: 'id' as never,\n fill: true,\n headingLevel: 3,\n onNew: undefined,\n newLabel: 'New',\n emptyLabel: 'No cards'\n})\n\nconst emit = defineEmits<{\n 'card-click': [item: T]\n /** A card dropped into a column, at its new index there. Show it moved at once; put it back if saving fails. */\n move: [id: unknown, column: string, index: number]\n}>()\n\ndefineSlots<{\n /** A card's content; `item.name` as a title by default. */\n card?: (props: { item: T; column: BoardColumn<T> }) => unknown\n /** Replaces a column's count, e.g. its summed deal value. */\n count?: (props: { column: BoardColumn<T> }) => unknown\n}>()\n\nconst id = useId()\nconst idOf = (item: T): unknown => item[props.rowKey ?? 'id']\n\nconst dragged = ref<{ id: unknown; column: string; index: number } | null>(null)\nconst target = ref<{ column: string; index: number } | null>(null)\n\nfunction onDragStart(event: DragEvent, item: T, column: string, index: number) {\n if (event.dataTransfer) {\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', String(idOf(item)))\n }\n dragged.value = { id: idOf(item), column, index }\n}\n\nfunction onDragEnd() {\n dragged.value = null\n target.value = null\n}\n\nfunction overColumn(event: DragEvent, column: BoardColumn<T>) {\n if (!dragged.value) return\n event.preventDefault()\n if (target.value?.column !== column.id) target.value = { column: column.id, index: column.items.length }\n}\n\n// The card's top half drops before it, the bottom half after.\nfunction overCard(event: DragEvent, column: string, index: number) {\n if (!dragged.value) return\n event.preventDefault()\n event.stopPropagation()\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const at = event.clientY < rect.top + rect.height / 2 ? index : index + 1\n if (target.value?.column !== column || target.value.index !== at) target.value = { column, index: at }\n}\n\nfunction onLeave(event: DragEvent) {\n if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node | null)) target.value = null\n}\n\nfunction onDrop(event: DragEvent, column: BoardColumn<T>) {\n event.preventDefault()\n const from = dragged.value\n if (from) {\n let index = target.value?.column === column.id ? target.value.index : column.items.length\n if (from.column === column.id) {\n // Within a column the dragged card leaves its old slot first.\n if (index > from.index) index--\n if (props.sortable && index !== from.index) emit('move', from.id, column.id, index)\n } else {\n emit('move', from.id, column.id, index)\n }\n }\n onDragEnd()\n}\n\nfunction onCardKeydown(event: KeyboardEvent, item: T) {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return\n event.preventDefault()\n emit('card-click', item)\n}\n\nconst isTarget = (column: string, index: number, item: T) =>\n target.value?.column === column && target.value.index === index && dragged.value?.id !== idOf(item)\n</script>\n\n<!-- A board: cards in columns by a field (a stage, a status), dragged between columns. -->\n<template>\n <div\n class=\"flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']\"\n :class=\"fill && 'h-full min-h-0 flex-1'\"\n >\n <section\n v-for=\"column in columns\"\n :key=\"column.id\"\n :aria-labelledby=\"`${id}-${column.id}`\"\n class=\"flex min-h-0 w-full max-w-xs min-w-[16rem] shrink-0 flex-col\"\n >\n <header class=\"flex items-center justify-between px-1 pb-2.5\">\n <component\n :is=\"`h${headingLevel}`\"\n :id=\"`${id}-${column.id}`\"\n class=\"m-0 flex items-center gap-2 text-sm font-semibold text-sui-ink\"\n >\n <Icon v-if=\"column.icon\" :color=\"column.iconColor ?? 'ink-subtle'\" :name=\"column.icon\" />\n {{ column.name }}\n </component>\n <span\n class=\"ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums\"\n >\n <slot :column=\"column\" name=\"count\">{{ column.items.length }}</slot>\n </span>\n </header>\n <div\n class=\"group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150\"\n :class=\"target?.column === column.id && 'ring-2 ring-sui-accent-line ring-inset'\"\n @dragleave=\"onLeave\"\n @dragover=\"overColumn($event, column)\"\n @drop=\"onDrop($event, column)\"\n >\n <template v-if=\"loading\">\n <div\n v-for=\"i in 3\"\n :key=\"i\"\n aria-hidden=\"true\"\n class=\"rounded-xl border border-sui-line-subtle bg-sui-surface p-4\"\n >\n <Skeleton class=\"mb-3\" />\n <Skeleton height=\"0.75rem\" width=\"70%\" />\n </div>\n </template>\n <p\n v-else-if=\"column.items.length === 0 && !dragged\"\n class=\"m-0 px-1 py-3 text-center text-13 text-sui-ink-muted\"\n >\n {{ emptyLabel }}\n </p>\n <ul v-else class=\"m-0 flex list-none flex-col gap-2.5 p-0\">\n <li v-for=\"(item, index) in column.items\" :key=\"idOf(item) as PropertyKey\">\n <div\n class=\"relative cursor-pointer rounded-xl border border-sui-line-subtle bg-sui-surface p-4 outline-hidden transition-[border-color,opacity] duration-150 hover:border-sui-line focus-visible:sui-ring\"\n :class=\"[\n dragged?.id === idOf(item) && 'opacity-40',\n isTarget(column.id, index, item) && 'shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]'\n ]\"\n draggable=\"true\"\n role=\"button\"\n tabindex=\"0\"\n @click=\"emit('card-click', item)\"\n @dragend=\"onDragEnd\"\n @dragover=\"overCard($event, column.id, index)\"\n @dragstart=\"onDragStart($event, item, column.id, index)\"\n @keydown=\"onCardKeydown($event, item)\"\n >\n <slot :column=\"column\" :item=\"item\" name=\"card\">\n <p class=\"m-0 text-sm font-semibold text-sui-ink\">{{ item.name }}</p>\n </slot>\n </div>\n </li>\n </ul>\n <button\n v-if=\"onNew && !loading\"\n class=\"flex h-9 w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-dashed border-sui-line-strong text-xs font-medium text-sui-ink-muted opacity-0 outline-hidden transition-all duration-150 group-hover/body:opacity-100 hover:border-sui-ink-muted hover:bg-sui-surface/60 hover:text-sui-ink-body focus-visible:opacity-100 focus-visible:sui-ring\"\n type=\"button\"\n @click=\"onNew(column)\"\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 14 14\"\n >\n <path d=\"M7 2.5v9M2.5 7h9\" />\n </svg>\n {{ newLabel }}\n </button>\n </div>\n </section>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"Board.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_Fragment","_renderList","_unref","_createElementVNode","_createBlock","_resolveDynamicComponent","_withCtx","Icon","_createCommentVNode","_createTextVNode","_toDisplayString","_renderSlot","_createVNode","Skeleton","_withModifiers","Icon_default","Skeleton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","MOUSE_SLOP","TOUCH_SLOP","TOUCH_DELAY","EDGE","frame","Board_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","id","useId","idOf","item","root","ref","dragged","target","pending","pressTimer","ghost","grab","justDragged","onPointerDown","event","touch","start","onPointerMove","onPointerUp","stop","holdScroll","onEscape","rect","follow","moved","aim","scrollNear","x","y","under","body","column","card","index","c","board","from","to","clickCard","onCardKeydown","isTarget","openBlock","createElementBlock","normalizeClass","Fragment","renderList","unref","createElementVNode","createBlock","resolveDynamicComponent","withCtx","createCommentVNode","createTextVNode","toDisplayString","renderSlot","_ctx","i","createVNode","$event","_cache","withModifiers"],"sources":["../../../src/components/Board/Board.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends Record<string, any>\">\nimport { onBeforeUnmount, ref, useId } from 'vue'\n\nimport Icon from '../Icon/Icon.vue'\nimport Skeleton from '../Skeleton/Skeleton.vue'\nimport type { BoardColumn } from './types'\n\nexport interface BoardProps<T> {\n columns: BoardColumn<T>[]\n /** The item field that identifies it. */\n rowKey?: keyof T & string\n /** Cards also reorder within a column, for records that carry a position. */\n sortable?: boolean\n /** Columns run to the bottom of the parent and scroll inside it; give the parent a height. False sizes them to their cards. */\n fill?: boolean\n loading?: boolean\n /** The columns' heading level, to fit the page's outline. */\n headingLevel?: 2 | 3 | 4 | 5 | 6\n /** Draws each card's border, padding and surface. False when the `card` slot draws its own. */\n cardFrame?: boolean\n /** Makes a card a button that opens on click, Enter and Space (`@card-click`). Without it, the card slot carries its\n * own link or buttons. */\n onCardClick?: (item: T) => void\n /** Adds a dashed \"New\" button at the foot of each column, shown on hover (`@new`). */\n onNew?: (column: BoardColumn<T>) => void\n newLabel?: string\n emptyLabel?: string\n}\n\nconst props = withDefaults(defineProps<BoardProps<T>>(), {\n rowKey: 'id' as never,\n fill: true,\n headingLevel: 3,\n cardFrame: true,\n onCardClick: undefined,\n onNew: undefined,\n newLabel: 'New',\n emptyLabel: 'No cards'\n})\n\nconst emit = defineEmits<{\n /** A card dropped into a column, at its new index there. Show it moved at once; put it back if saving fails. */\n move: [id: unknown, column: string, index: number]\n}>()\n\ndefineSlots<{\n /** A card's content; `item.name` as a title by default. */\n card?: (props: { item: T; column: BoardColumn<T> }) => unknown\n /** Replaces a column's count, e.g. its summed deal value. */\n count?: (props: { column: BoardColumn<T> }) => unknown\n}>()\n\nconst id = useId()\nconst idOf = (item: T): unknown => item[props.rowKey ?? 'id']\nconst root = ref<HTMLElement>()\n\nconst dragged = ref<{ id: unknown; column: string; index: number } | null>(null)\nconst target = ref<{ column: string; index: number } | null>(null)\n\n// Dragging runs on pointer events, so it works with a mouse, a pen and a finger. A mouse drags once it moves a few\n// pixels, so a click still clicks; a finger after a short press, so a swipe still scrolls.\nconst MOUSE_SLOP = 4\nconst TOUCH_SLOP = 8\nconst TOUCH_DELAY = 250\nconst EDGE = 48\n\nlet pending: {\n item: T\n column: string\n index: number\n x: number\n y: number\n card: HTMLElement\n touch: boolean\n} | null = null\nlet pressTimer: number | undefined\nlet ghost: HTMLElement | null = null\nlet grab = { x: 0, y: 0 }\nlet justDragged = false\n\nfunction onPointerDown(event: PointerEvent, item: T, column: string, index: number) {\n if (event.button !== 0 || (event.target as HTMLElement).closest('input, textarea, select, [contenteditable]')) return\n const touch = event.pointerType === 'touch'\n pending = { item, column, index, x: event.clientX, y: event.clientY, card: event.currentTarget as HTMLElement, touch }\n if (touch) pressTimer = window.setTimeout(() => pending && start(), TOUCH_DELAY)\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', stop)\n window.addEventListener('touchmove', holdScroll, { passive: false })\n window.addEventListener('keydown', onEscape)\n}\n\nfunction start() {\n if (!pending) return\n const rect = pending.card.getBoundingClientRect()\n grab = { x: pending.x - rect.left, y: pending.y - rect.top }\n ghost = pending.card.cloneNode(true) as HTMLElement\n ghost.removeAttribute('id')\n ghost.setAttribute('aria-hidden', 'true')\n ghost.classList.add('pointer-events-none', 'fixed', 'top-0', 'left-0', 'z-[1300]', 'shadow-lg', 'opacity-95')\n ghost.style.width = `${rect.width}px`\n document.body.appendChild(ghost)\n follow(pending.x, pending.y)\n dragged.value = { id: idOf(pending.item), column: pending.column, index: pending.index }\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!pending) return\n if (!dragged.value) {\n const moved = Math.hypot(event.clientX - pending.x, event.clientY - pending.y)\n // A finger that moves before the press is long enough is scrolling.\n if (pending.touch) return moved > TOUCH_SLOP ? stop() : undefined\n if (moved < MOUSE_SLOP) return\n start()\n }\n event.preventDefault()\n follow(event.clientX, event.clientY)\n aim(event.clientX, event.clientY)\n scrollNear(event.clientX, event.clientY)\n}\n\nfunction follow(x: number, y: number) {\n if (ghost) ghost.style.transform = `translate(${x - grab.x}px, ${y - grab.y}px) rotate(1.5deg)`\n}\n\n// What's under the pointer: a card's top half drops before it, the bottom half after; an empty spot, at the end.\nfunction aim(x: number, y: number) {\n const under = document.elementFromPoint(x, y)\n const body = under?.closest<HTMLElement>('[data-board-column]')\n if (!body || !root.value?.contains(body)) {\n target.value = null\n return\n }\n const column = body.dataset.boardColumn!\n const card = under!.closest<HTMLElement>('[data-board-index]')\n let index = props.columns.find((c) => c.id === column)?.items.length ?? 0\n if (card && body.contains(card)) {\n const rect = card.getBoundingClientRect()\n index = Number(card.dataset.boardIndex) + (y < rect.top + rect.height / 2 ? 0 : 1)\n }\n if (target.value?.column !== column || target.value.index !== index) target.value = { column, index }\n}\n\n// Near a column's top or bottom, or the board's sides, it scrolls that way.\nfunction scrollNear(x: number, y: number) {\n const body = document.elementFromPoint(x, y)?.closest<HTMLElement>('[data-board-column]')\n if (body) {\n const rect = body.getBoundingClientRect()\n if (y < rect.top + EDGE) body.scrollTop -= 12\n else if (y > rect.bottom - EDGE) body.scrollTop += 12\n }\n const board = root.value?.getBoundingClientRect()\n if (board && root.value) {\n if (x < board.left + EDGE) root.value.scrollLeft -= 12\n else if (x > board.right - EDGE) root.value.scrollLeft += 12\n }\n}\n\nfunction onPointerUp() {\n const from = dragged.value\n const to = target.value\n if (from && to) {\n let index = to.index\n if (from.column === to.column) {\n // Within a column the dragged card leaves its old slot first.\n if (index > from.index) index--\n if (props.sortable && index !== from.index) emit('move', from.id, to.column, index)\n } else {\n emit('move', from.id, to.column, index)\n }\n }\n stop()\n}\n\nfunction holdScroll(event: TouchEvent) {\n if (dragged.value) event.preventDefault()\n}\n\nfunction onEscape(event: KeyboardEvent) {\n if (event.key === 'Escape' && dragged.value) {\n event.preventDefault()\n stop()\n }\n}\n\nfunction stop() {\n window.clearTimeout(pressTimer)\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', stop)\n window.removeEventListener('touchmove', holdScroll)\n window.removeEventListener('keydown', onEscape)\n ghost?.remove()\n ghost = null\n // The click that ends a drag isn't a click on the card.\n if (dragged.value) {\n justDragged = true\n window.setTimeout(() => (justDragged = false))\n }\n pending = null\n dragged.value = null\n target.value = null\n}\n\nonBeforeUnmount(stop)\n\nfunction clickCard(event: MouseEvent, item: T) {\n if (justDragged) {\n event.preventDefault()\n event.stopPropagation()\n return\n }\n props.onCardClick?.(item)\n}\n\nfunction onCardKeydown(event: KeyboardEvent, item: T) {\n if (!props.onCardClick || event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return\n event.preventDefault()\n props.onCardClick(item)\n}\n\nconst isTarget = (column: string, index: number, item: T) =>\n target.value?.column === column && target.value.index === index && dragged.value?.id !== idOf(item)\n\nconst frame =\n 'border border-sui-line-subtle bg-sui-surface p-4 transition-[border-color,opacity] duration-150 hover:border-sui-line'\n</script>\n\n<!-- A board: cards in columns by a field (a stage, a status), dragged between columns. -->\n<template>\n <div\n ref=\"root\"\n class=\"flex items-stretch gap-5 overflow-x-auto pb-2 after:w-1 after:shrink-0 after:content-['']\"\n :class=\"fill && 'h-full min-h-0 flex-1'\"\n >\n <section\n v-for=\"column in columns\"\n :key=\"column.id\"\n :aria-labelledby=\"`${id}-${column.id}`\"\n class=\"flex min-h-0 w-full max-w-xs min-w-[16rem] shrink-0 flex-col\"\n >\n <header class=\"flex items-center justify-between px-1 pb-2.5\">\n <component\n :is=\"`h${headingLevel}`\"\n :id=\"`${id}-${column.id}`\"\n class=\"m-0 flex items-center gap-2 text-sm font-semibold text-sui-ink\"\n >\n <Icon v-if=\"column.icon\" :color=\"column.iconColor ?? 'ink-subtle'\" :name=\"column.icon\" />\n {{ column.name }}\n </component>\n <span\n class=\"ml-2 inline-flex h-5 items-center justify-center rounded-full bg-sui-sunken px-2 text-xs text-sui-ink-muted tabular-nums\"\n >\n <slot :column=\"column\" name=\"count\">{{ column.items.length }}</slot>\n </span>\n </header>\n <div\n class=\"group/body flex min-h-[120px] flex-1 flex-col gap-2.5 overflow-y-auto rounded-xl bg-sui-sunken p-2.5 transition-shadow duration-150\"\n :class=\"target?.column === column.id && 'ring-2 ring-sui-accent-line ring-inset'\"\n :data-board-column=\"column.id\"\n >\n <template v-if=\"loading\">\n <div\n v-for=\"i in 3\"\n :key=\"i\"\n aria-hidden=\"true\"\n class=\"rounded-xl border border-sui-line-subtle bg-sui-surface p-4\"\n >\n <Skeleton class=\"mb-3\" />\n <Skeleton height=\"0.75rem\" width=\"70%\" />\n </div>\n </template>\n <p\n v-else-if=\"column.items.length === 0 && !dragged\"\n class=\"m-0 px-1 py-3 text-center text-13 text-sui-ink-muted\"\n >\n {{ emptyLabel }}\n </p>\n <ul v-else class=\"m-0 flex list-none flex-col gap-2.5 p-0\">\n <li v-for=\"(item, index) in column.items\" :key=\"idOf(item) as PropertyKey\">\n <div\n class=\"relative rounded-xl outline-hidden transition-opacity duration-150 select-none\"\n :class=\"[\n cardFrame && frame,\n onCardClick && 'cursor-pointer focus-visible:sui-ring',\n dragged?.id === idOf(item) && 'opacity-40',\n isTarget(column.id, index, item) && 'shadow-[0_-6px_0_-4px_rgb(var(--sui-accent-solid))]'\n ]\"\n :data-board-index=\"index\"\n :role=\"onCardClick ? 'button' : undefined\"\n :tabindex=\"onCardClick ? 0 : undefined\"\n @click.capture=\"clickCard($event, item)\"\n @contextmenu=\"dragged && $event.preventDefault()\"\n @dragstart.prevent\n @keydown=\"onCardKeydown($event, item)\"\n @pointerdown=\"onPointerDown($event, item, column.id, index)\"\n >\n <slot :column=\"column\" :item=\"item\" name=\"card\">\n <p class=\"m-0 text-sm font-semibold text-sui-ink\">{{ item.name }}</p>\n </slot>\n </div>\n </li>\n </ul>\n <button\n v-if=\"onNew && !loading\"\n class=\"flex h-9 w-full shrink-0 items-center justify-center gap-1.5 rounded-lg border border-dashed border-sui-line-strong text-xs font-medium text-sui-ink-muted opacity-0 outline-hidden transition-all duration-150 group-hover/body:opacity-100 hover:border-sui-ink-muted hover:bg-sui-surface/60 hover:text-sui-ink-body focus-visible:opacity-100 focus-visible:sui-ring\"\n type=\"button\"\n @click=\"onNew(column)\"\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 14 14\"\n >\n <path d=\"M7 2.5v9M2.5 7h9\" />\n </svg>\n {{ newLabel }}\n </button>\n </div>\n </section>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FilterChip.js","names":["_sfc_main","FilterChip_vue_vue_type_script_setup_true_lang_default","FilterChip_default"],"sources":["../../../src/components/Filters/FilterChip.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs } from 'vue'\n\nimport {\n DropdownMenuContent,\n DropdownMenuPortal,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuRoot,\n DropdownMenuTrigger,\n PopoverContent,\n PopoverPortal,\n PopoverRoot,\n PopoverTrigger,\n RadioGroupItem,\n RadioGroupRoot\n} from 'reka-ui'\n\nimport Checkbox from '../Checkbox/Checkbox.vue'\nimport Icon from '../Icon/Icon.vue'\nimport Input from '../Input/Input.vue'\nimport SegmentedControl from '../SegmentedControl/SegmentedControl.vue'\nimport CheckIcon from '../internal/CheckIcon.vue'\nimport FilterFrame from './FilterFrame.vue'\nimport FilterGlyph from './FilterGlyph.vue'\nimport { datePresets as defaultDatePresets, describeFilter, filterOperators } from './defaults'\nimport type { FilterFieldType, FilterOperator, FilterOption, FilterValue } from './types'\n\nexport interface FilterChipProps {\n /** A filter, read as a sentence; `add` is the quiet \"+ Filter\" that starts one (FilterMenu includes it). */\n kind?: 'filter' | 'add'\n field?: string\n /** Picks the editor: a checklist for enum, person and company, an input for text and number, presets for date, Yes / No for boolean. */\n type?: FilterFieldType\n /** An app icon for the field, instead of the type's glyph. */\n icon?: string\n /** The operators offered; English ones by type by default. */\n operators?: FilterOperator[]\n /** The choices for enum, person and company. */\n options?: FilterOption[]\n /** The live result count in the panel's footer, e.g. \"120 people\". */\n status?: string\n /** The plural noun once more than two values are picked: \"3 stages\". */\n valueLabel?: string\n datePresets?: string[]\n /** Adds a Remove action to the panel (`@remove`). */\n onRemove?: () => void\n removeLabel?: string\n addLabel?: string\n operatorLabel?: string\n searchPlaceholder?: string\n valuePlaceholder?: string\n doneLabel?: string\n closeLabel?: string\n yesLabel?: string\n noLabel?: string\n}\n\n// Attributes and listeners go to the chip itself; the root is a renderless popover.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilterChipProps>(), {\n kind: 'filter',\n field: undefined,\n type: 'enum',\n icon: undefined,\n operators: undefined,\n options: () => [],\n status: undefined,\n valueLabel: 'values',\n datePresets: () => defaultDatePresets,\n onRemove: undefined,\n removeLabel: 'Remove',\n addLabel: 'Filter',\n operatorLabel: 'Operator',\n searchPlaceholder: 'Search…',\n valuePlaceholder: 'Type a value',\n doneLabel: 'Done',\n closeLabel: 'Close',\n yesLabel: 'Yes',\n noLabel: 'No'\n})\n\nconst op = defineModel<string>('op')\nconst value = defineModel<FilterValue>({ default: null })\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** Your own editor, in place of the type's. */\n default?: () => unknown\n}>()\n\nconst attrs = useAttrs()\n\nconst ops = computed(() => props.operators ?? filterOperators[props.type])\nconst current = computed(() => ops.value.find((o) => o.value === op.value))\nconst needsValue = computed(() => !current.value?.noValue)\nconst text = computed(() =>\n needsValue.value\n ? describeFilter(value.value, props.options, {\n valueLabel: props.valueLabel,\n yesLabel: props.yesLabel,\n noLabel: props.noLabel\n })\n : ''\n)\nconst isChecklist = computed(() => ['enum', 'person', 'company'].includes(props.type))\n\nconst query = ref('')\nconst listed = computed(() => {\n const q = query.value.trim().toLowerCase()\n return q ? props.options.filter((o) => o.label.toLowerCase().includes(q)) : props.options\n})\n\nconst picked = computed(() => (Array.isArray(value.value) ? value.value : []))\n\nfunction toggle(choice: string | number) {\n value.value = picked.value.includes(choice) ? picked.value.filter((v) => v !== choice) : [...picked.value, choice]\n}\n\nfunction remove() {\n open.value = false\n props.onRemove?.()\n}\n\nconst chip =\n 'inline-flex h-7.5 items-center gap-1.5 rounded-lg border text-13 whitespace-nowrap outline-hidden transition-[border-color,box-shadow] duration-150 focus-visible:sui-ring'\nconst row =\n 'flex min-h-8 w-full cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1 text-left text-13.5 outline-hidden transition-colors duration-100 focus-visible:sui-ring-inset'\nconst rowOn = 'bg-sui-accent-wash font-semibold text-sui-accent'\nconst rowOff = 'text-sui-ink hover:bg-sui-sunken'\nconst hueDots: Record<string, string> = {\n blue: 'bg-sui-hue-blue-ink',\n green: 'bg-sui-hue-green-ink',\n orange: 'bg-sui-hue-orange-ink',\n pink: 'bg-sui-hue-pink-ink',\n purple: 'bg-sui-hue-purple-ink',\n slate: 'bg-sui-hue-slate-ink',\n red: 'bg-sui-hue-red-ink',\n amber: 'bg-sui-hue-amber-ink'\n}\n</script>\n\n<!-- A view's filter as a sentence chip (\"Stage is any of Lead, Customer\"), whose panel changes it as you go. -->\n<template>\n <button\n v-if=\"kind === 'add'\"\n v-bind=\"attrs\"\n class=\"border-dashed border-sui-line-strong px-2 font-medium text-sui-ink-muted hover:border-sui-ink-muted hover:text-sui-ink-body\"\n :class=\"chip\"\n type=\"button\"\n >\n <FilterGlyph name=\"plus\" />\n {{ addLabel }}\n </button>\n <PopoverRoot v-else v-model:open=\"open\">\n <PopoverTrigger\n v-bind=\"attrs\"\n class=\"border-sui-accent-line bg-sui-accent-wash pr-1.5 pl-2 text-sui-accent hover:border-sui-accent-bright data-[state=open]:border-sui-accent-solid data-[state=open]:shadow-[0_0_0_3px_rgb(var(--sui-focus-ring))]\"\n :class=\"chip\"\n >\n <Icon v-if=\"icon\" class=\"size-3.5 opacity-80\" color=\"accent\" :name=\"icon\" />\n <FilterGlyph v-else class=\"opacity-80\" :name=\"type\" />\n <!-- Real spaces between the words, so the chip's name reads as a sentence. -->\n <span\n ><span class=\"font-medium\">{{ field }}</span\n ><template v-if=\"current\">{{ ' ' }}{{ current.label }}</template\n ><template v-if=\"text\"\n >{{ ' ' }}<span class=\"font-semibold\">{{ text }}</span></template\n ></span\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 shrink-0 fill-none stroke-current opacity-70\"\n stroke-linecap=\"round\"\n stroke-width=\"2.2\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </PopoverTrigger>\n <PopoverPortal>\n <PopoverContent\n align=\"start\"\n class=\"z-[1100] w-85 max-w-[calc(100vw-2rem)] origin-(--reka-popover-content-transform-origin) rounded-xl border border-sui-line bg-sui-surface text-sui-ink shadow-lg data-[state=open]:animate-sui-overlay-in\"\n :side-offset=\"8\"\n >\n <FilterFrame :close-label=\"closeLabel\" :enter-label=\"doneLabel\" :status=\"status\" @enter=\"open = false\">\n <div class=\"flex h-11 items-center gap-1.5 pr-1.5 pl-3.5 text-sui-ink-muted\">\n <Icon v-if=\"icon\" color=\"ink-muted\" :name=\"icon\" />\n <FilterGlyph v-else class=\"size-4\" :name=\"type\" />\n <span class=\"text-sm font-semibold text-sui-ink\">{{ field }}</span>\n <DropdownMenuRoot v-if=\"ops.length > 1\">\n <DropdownMenuTrigger\n :aria-label=\"`${operatorLabel}: ${current?.label ?? ''}`\"\n class=\"inline-flex h-7 items-center gap-0.5 rounded-md pr-1 pl-1.5 text-13.5 font-medium text-sui-accent outline-hidden transition-colors duration-150 hover:bg-sui-accent-wash focus-visible:sui-ring-inset data-[state=open]:bg-sui-accent-wash\"\n >\n {{ current?.label }}\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"2.2\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </DropdownMenuTrigger>\n <DropdownMenuPortal>\n <DropdownMenuContent\n align=\"start\"\n class=\"z-[1100] w-44 rounded-lg border border-sui-line bg-sui-surface p-1 shadow-lg data-[state=open]:animate-sui-menu-in\"\n :side-offset=\"4\"\n >\n <DropdownMenuRadioGroup v-model=\"op\">\n <DropdownMenuRadioItem\n v-for=\"operator in ops\"\n :key=\"operator.value\"\n class=\"flex h-8 cursor-pointer items-center justify-between rounded-md px-2.5 text-13.5 text-sui-ink-body outline-hidden select-none data-highlighted:bg-sui-sunken data-[state=checked]:font-semibold data-[state=checked]:text-sui-ink\"\n :value=\"operator.value\"\n >\n {{ operator.label }}\n <CheckIcon v-if=\"operator.value === op\" />\n </DropdownMenuRadioItem>\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenuPortal>\n </DropdownMenuRoot>\n <span v-else class=\"text-13.5 text-sui-ink-muted\">{{ current?.label }}</span>\n <button\n v-if=\"onRemove\"\n class=\"ml-auto inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-12.5 font-medium text-sui-ink-muted outline-hidden transition-colors duration-150 hover:bg-sui-danger-soft hover:text-sui-danger focus-visible:sui-ring-inset\"\n type=\"button\"\n @click=\"remove\"\n >\n <FilterGlyph name=\"trash\" />\n {{ removeLabel }}\n </button>\n </div>\n\n <slot v-if=\"$slots.default\" />\n <template v-else-if=\"needsValue\">\n <template v-if=\"isChecklist\">\n <div\n v-if=\"options.length > 6\"\n class=\"mx-2 mb-1 flex h-9 items-center gap-2 rounded-lg border border-sui-line-subtle px-2.5 text-sui-ink-muted\"\n >\n <FilterGlyph class=\"size-4\" name=\"search\" />\n <!-- focus: classes too: an app's forms plugin rings focused text fields, and outranks plain utilities. -->\n <input\n v-model=\"query\"\n :aria-label=\"searchPlaceholder\"\n autofocus\n class=\"h-full min-w-0 flex-1 border-none bg-transparent p-0 text-sm text-sui-ink shadow-none outline-hidden placeholder:text-sui-ink-faint focus:border-none focus:shadow-none focus:ring-0 focus:outline-hidden\"\n :placeholder=\"searchPlaceholder\"\n />\n </div>\n <div class=\"max-h-64 overflow-auto px-1.5 pt-1 pb-1.5\">\n <label\n v-for=\"option in listed\"\n :key=\"option.value\"\n :class=\"[row, picked.includes(option.value) ? rowOn : rowOff]\"\n >\n <Checkbox :model-value=\"picked.includes(option.value)\" @update:model-value=\"toggle(option.value)\" />\n <span v-if=\"option.color\" class=\"size-1.5 shrink-0 rounded-full\" :class=\"hueDots[option.color]\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ option.label }}</span>\n <span v-if=\"option.count != null\" class=\"text-xs font-normal text-sui-ink-muted tabular-nums\">{{\n option.count\n }}</span>\n </label>\n </div>\n </template>\n <div v-else-if=\"type === 'text' || type === 'number'\" class=\"px-2 pt-1 pb-2\">\n <Input\n :aria-label=\"field\"\n autofocus\n :model-value=\"value == null ? '' : String(value)\"\n :placeholder=\"valuePlaceholder\"\n :type=\"type === 'number' ? 'number' : 'text'\"\n @update:model-value=\"value = $event ?? null\"\n />\n </div>\n <RadioGroupRoot\n v-else-if=\"type === 'date'\"\n :aria-label=\"field\"\n class=\"max-h-64 overflow-auto px-1.5 pt-1 pb-1.5\"\n :model-value=\"typeof value === 'string' ? value : undefined\"\n @update:model-value=\"value = String($event)\"\n >\n <RadioGroupItem\n v-for=\"preset in datePresets\"\n :key=\"preset\"\n :class=\"[row, preset === value ? rowOn : rowOff]\"\n :value=\"preset\"\n >\n <FilterGlyph\n class=\"size-4\"\n :class=\"preset === value ? 'text-sui-accent' : 'text-sui-ink-muted'\"\n name=\"date\"\n />\n <span class=\"min-w-0 flex-1 truncate\">{{ preset }}</span>\n <CheckIcon v-if=\"preset === value\" />\n </RadioGroupItem>\n </RadioGroupRoot>\n <div v-else-if=\"type === 'boolean'\" class=\"px-2.5 pt-1 pb-2.5\">\n <SegmentedControl\n :aria-label=\"field\"\n :model-value=\"value ? 'yes' : 'no'\"\n :options=\"[\n { value: 'yes', label: yesLabel },\n { value: 'no', label: noLabel }\n ]\"\n @update:model-value=\"value = $event === 'yes'\"\n />\n </div>\n </template>\n </FilterFrame>\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"FilterChip.js","names":["_sfc_main","FilterChip_vue_vue_type_script_setup_true_lang_default","FilterChip_default"],"sources":["../../../src/components/Filters/FilterChip.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs } from 'vue'\n\nimport {\n DropdownMenuContent,\n DropdownMenuPortal,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuRoot,\n DropdownMenuTrigger,\n PopoverContent,\n PopoverPortal,\n PopoverRoot,\n PopoverTrigger,\n RadioGroupItem,\n RadioGroupRoot\n} from 'reka-ui'\n\nimport Checkbox from '../Checkbox/Checkbox.vue'\nimport Icon from '../Icon/Icon.vue'\nimport Input from '../Input/Input.vue'\nimport SegmentedControl from '../SegmentedControl/SegmentedControl.vue'\nimport CheckIcon from '../internal/CheckIcon.vue'\nimport FilterFrame from './FilterFrame.vue'\nimport FilterGlyph from './FilterGlyph.vue'\nimport { datePresets as defaultDatePresets, describeFilter, filterOperators } from './defaults'\nimport type { FilterFieldType, FilterOperator, FilterOption, FilterSummary, FilterValue } from './types'\n\nexport interface FilterChipProps {\n /** A filter, read as a sentence; `add` is the quiet \"+ Filter\" that starts one (FilterMenu includes it). */\n kind?: 'filter' | 'add'\n field?: string\n /** Picks the editor: a checklist for enum, person and company, an input for text and number, presets for date, Yes / No for boolean. */\n type?: FilterFieldType\n /** An app icon for the field, instead of the type's glyph. */\n icon?: string\n /** The operators offered; English ones by type by default. */\n operators?: FilterOperator[]\n /** The choices for enum, person and company. */\n options?: FilterOption[]\n /** Your own wording for the chip, in place of field, operator and value: \"Not archived\", \"Created in the last 7 days\". */\n summary?: FilterSummary\n /** The live result count in the panel's footer, e.g. \"120 people\". */\n status?: string\n /** The plural noun once more than two values are picked: \"3 stages\". */\n valueLabel?: string\n datePresets?: string[]\n /** Adds a Remove action to the panel (`@remove`). */\n onRemove?: () => void\n removeLabel?: string\n addLabel?: string\n operatorLabel?: string\n searchPlaceholder?: string\n valuePlaceholder?: string\n doneLabel?: string\n closeLabel?: string\n yesLabel?: string\n noLabel?: string\n}\n\n// Attributes and listeners go to the chip itself; the root is a renderless popover.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FilterChipProps>(), {\n kind: 'filter',\n field: undefined,\n type: 'enum',\n icon: undefined,\n operators: undefined,\n options: () => [],\n summary: undefined,\n status: undefined,\n valueLabel: 'values',\n datePresets: () => defaultDatePresets,\n onRemove: undefined,\n removeLabel: 'Remove',\n addLabel: 'Filter',\n operatorLabel: 'Operator',\n searchPlaceholder: 'Search…',\n valuePlaceholder: 'Type a value',\n doneLabel: 'Done',\n closeLabel: 'Close',\n yesLabel: 'Yes',\n noLabel: 'No'\n})\n\nconst op = defineModel<string>('op')\nconst value = defineModel<FilterValue>({ default: null })\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** Your own editor, in place of the type's. */\n default?: () => unknown\n}>()\n\nconst attrs = useAttrs()\n\nconst ops = computed(() => props.operators ?? filterOperators[props.type])\nconst current = computed(() => ops.value.find((o) => o.value === op.value))\nconst needsValue = computed(() => !current.value?.noValue)\nconst text = computed(() =>\n needsValue.value\n ? describeFilter(value.value, props.options, {\n valueLabel: props.valueLabel,\n yesLabel: props.yesLabel,\n noLabel: props.noLabel\n })\n : ''\n)\nconst words = computed(() => ({\n field: props.summary?.field ?? props.field,\n op: props.summary ? props.summary.op : current.value?.label,\n value: props.summary ? props.summary.value : text.value\n}))\nconst isChecklist = computed(() => ['enum', 'person', 'company'].includes(props.type))\n\nconst query = ref('')\nconst listed = computed(() => {\n const q = query.value.trim().toLowerCase()\n return q ? props.options.filter((o) => o.label.toLowerCase().includes(q)) : props.options\n})\n\nconst picked = computed(() => (Array.isArray(value.value) ? value.value : []))\n\nfunction toggle(choice: string | number) {\n value.value = picked.value.includes(choice) ? picked.value.filter((v) => v !== choice) : [...picked.value, choice]\n}\n\nfunction remove() {\n open.value = false\n props.onRemove?.()\n}\n\nconst chip =\n 'inline-flex h-7.5 items-center gap-1.5 rounded-lg border text-13 whitespace-nowrap outline-hidden transition-[border-color,box-shadow] duration-150 focus-visible:sui-ring'\nconst row =\n 'flex min-h-8 w-full cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1 text-left text-13.5 outline-hidden transition-colors duration-100 focus-visible:sui-ring-inset'\nconst rowOn = 'bg-sui-accent-wash font-semibold text-sui-accent'\nconst rowOff = 'text-sui-ink hover:bg-sui-sunken'\nconst hueDots: Record<string, string> = {\n blue: 'bg-sui-hue-blue-ink',\n green: 'bg-sui-hue-green-ink',\n orange: 'bg-sui-hue-orange-ink',\n pink: 'bg-sui-hue-pink-ink',\n purple: 'bg-sui-hue-purple-ink',\n slate: 'bg-sui-hue-slate-ink',\n red: 'bg-sui-hue-red-ink',\n amber: 'bg-sui-hue-amber-ink'\n}\n</script>\n\n<!-- A view's filter as a sentence chip (\"Stage is any of Lead, Customer\"), whose panel changes it as you go. -->\n<template>\n <button\n v-if=\"kind === 'add'\"\n v-bind=\"attrs\"\n class=\"border-dashed border-sui-line-strong px-2 font-medium text-sui-ink-muted hover:border-sui-ink-muted hover:text-sui-ink-body\"\n :class=\"chip\"\n type=\"button\"\n >\n <FilterGlyph name=\"plus\" />\n {{ addLabel }}\n </button>\n <PopoverRoot v-else v-model:open=\"open\">\n <PopoverTrigger\n v-bind=\"attrs\"\n class=\"border-sui-accent-line bg-sui-accent-wash pr-1.5 pl-2 text-sui-accent hover:border-sui-accent-bright data-[state=open]:border-sui-accent-solid data-[state=open]:shadow-[0_0_0_3px_rgb(var(--sui-focus-ring))]\"\n :class=\"chip\"\n >\n <Icon v-if=\"icon\" class=\"size-3.5 opacity-80\" color=\"accent\" :name=\"icon\" />\n <FilterGlyph v-else class=\"opacity-80\" :name=\"type\" />\n <!-- Real spaces between the words, so the chip's name reads as a sentence. -->\n <span\n ><span class=\"font-medium\">{{ words.field }}</span\n ><template v-if=\"words.op\">{{ ' ' }}{{ words.op }}</template\n ><template v-if=\"words.value\"\n >{{ ' ' }}<span class=\"font-semibold\">{{ words.value }}</span></template\n ></span\n >\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 shrink-0 fill-none stroke-current opacity-70\"\n stroke-linecap=\"round\"\n stroke-width=\"2.2\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </PopoverTrigger>\n <PopoverPortal>\n <PopoverContent\n align=\"start\"\n class=\"z-[1100] w-85 max-w-[calc(100vw-2rem)] origin-(--reka-popover-content-transform-origin) rounded-xl border border-sui-line bg-sui-surface text-sui-ink shadow-lg data-[state=open]:animate-sui-overlay-in\"\n :side-offset=\"8\"\n >\n <FilterFrame :close-label=\"closeLabel\" :enter-label=\"doneLabel\" :status=\"status\" @enter=\"open = false\">\n <div class=\"flex h-11 items-center gap-1.5 pr-1.5 pl-3.5 text-sui-ink-muted\">\n <Icon v-if=\"icon\" color=\"ink-muted\" :name=\"icon\" />\n <FilterGlyph v-else class=\"size-4\" :name=\"type\" />\n <span class=\"text-sm font-semibold text-sui-ink\">{{ field }}</span>\n <DropdownMenuRoot v-if=\"ops.length > 1\">\n <DropdownMenuTrigger\n :aria-label=\"`${operatorLabel}: ${current?.label ?? ''}`\"\n class=\"inline-flex h-7 items-center gap-0.5 rounded-md pr-1 pl-1.5 text-13.5 font-medium text-sui-accent outline-hidden transition-colors duration-150 hover:bg-sui-accent-wash focus-visible:sui-ring-inset data-[state=open]:bg-sui-accent-wash\"\n >\n {{ current?.label }}\n <svg\n aria-hidden=\"true\"\n class=\"size-3.5 fill-none stroke-current\"\n stroke-linecap=\"round\"\n stroke-width=\"2.2\"\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </DropdownMenuTrigger>\n <DropdownMenuPortal>\n <DropdownMenuContent\n align=\"start\"\n class=\"z-[1100] w-44 rounded-lg border border-sui-line bg-sui-surface p-1 shadow-lg data-[state=open]:animate-sui-menu-in\"\n :side-offset=\"4\"\n >\n <DropdownMenuRadioGroup v-model=\"op\">\n <DropdownMenuRadioItem\n v-for=\"operator in ops\"\n :key=\"operator.value\"\n class=\"flex h-8 cursor-pointer items-center justify-between rounded-md px-2.5 text-13.5 text-sui-ink-body outline-hidden select-none data-highlighted:bg-sui-sunken data-[state=checked]:font-semibold data-[state=checked]:text-sui-ink\"\n :value=\"operator.value\"\n >\n {{ operator.label }}\n <CheckIcon v-if=\"operator.value === op\" />\n </DropdownMenuRadioItem>\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenuPortal>\n </DropdownMenuRoot>\n <span v-else class=\"text-13.5 text-sui-ink-muted\">{{ current?.label }}</span>\n <button\n v-if=\"onRemove\"\n class=\"ml-auto inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-12.5 font-medium text-sui-ink-muted outline-hidden transition-colors duration-150 hover:bg-sui-danger-soft hover:text-sui-danger focus-visible:sui-ring-inset\"\n type=\"button\"\n @click=\"remove\"\n >\n <FilterGlyph name=\"trash\" />\n {{ removeLabel }}\n </button>\n </div>\n\n <slot v-if=\"$slots.default\" />\n <template v-else-if=\"needsValue\">\n <template v-if=\"isChecklist\">\n <div\n v-if=\"options.length > 6\"\n class=\"mx-2 mb-1 flex h-9 items-center gap-2 rounded-lg border border-sui-line-subtle px-2.5 text-sui-ink-muted\"\n >\n <FilterGlyph class=\"size-4\" name=\"search\" />\n <!-- focus: classes too: an app's forms plugin rings focused text fields, and outranks plain utilities. -->\n <input\n v-model=\"query\"\n :aria-label=\"searchPlaceholder\"\n autofocus\n class=\"h-full min-w-0 flex-1 border-none bg-transparent p-0 text-sm text-sui-ink shadow-none outline-hidden placeholder:text-sui-ink-faint focus:border-none focus:shadow-none focus:ring-0 focus:outline-hidden\"\n :placeholder=\"searchPlaceholder\"\n />\n </div>\n <div class=\"max-h-64 overflow-auto px-1.5 pt-1 pb-1.5\">\n <label\n v-for=\"option in listed\"\n :key=\"option.value\"\n :class=\"[row, picked.includes(option.value) ? rowOn : rowOff]\"\n >\n <Checkbox :model-value=\"picked.includes(option.value)\" @update:model-value=\"toggle(option.value)\" />\n <span v-if=\"option.color\" class=\"size-1.5 shrink-0 rounded-full\" :class=\"hueDots[option.color]\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ option.label }}</span>\n <span v-if=\"option.count != null\" class=\"text-xs font-normal text-sui-ink-muted tabular-nums\">{{\n option.count\n }}</span>\n </label>\n </div>\n </template>\n <div v-else-if=\"type === 'text' || type === 'number'\" class=\"px-2 pt-1 pb-2\">\n <Input\n :aria-label=\"field\"\n autofocus\n :model-value=\"value == null ? '' : String(value)\"\n :placeholder=\"valuePlaceholder\"\n :type=\"type === 'number' ? 'number' : 'text'\"\n @update:model-value=\"value = $event ?? null\"\n />\n </div>\n <RadioGroupRoot\n v-else-if=\"type === 'date'\"\n :aria-label=\"field\"\n class=\"max-h-64 overflow-auto px-1.5 pt-1 pb-1.5\"\n :model-value=\"typeof value === 'string' ? value : undefined\"\n @update:model-value=\"value = String($event)\"\n >\n <RadioGroupItem\n v-for=\"preset in datePresets\"\n :key=\"preset\"\n :class=\"[row, preset === value ? rowOn : rowOff]\"\n :value=\"preset\"\n >\n <FilterGlyph\n class=\"size-4\"\n :class=\"preset === value ? 'text-sui-accent' : 'text-sui-ink-muted'\"\n name=\"date\"\n />\n <span class=\"min-w-0 flex-1 truncate\">{{ preset }}</span>\n <CheckIcon v-if=\"preset === value\" />\n </RadioGroupItem>\n </RadioGroupRoot>\n <div v-else-if=\"type === 'boolean'\" class=\"px-2.5 pt-1 pb-2.5\">\n <SegmentedControl\n :aria-label=\"field\"\n :model-value=\"value === null ? '' : value ? 'yes' : 'no'\"\n :options=\"[\n { value: 'yes', label: yesLabel },\n { value: 'no', label: noLabel }\n ]\"\n @update:model-value=\"value = $event === 'yes'\"\n />\n </div>\n </template>\n </FilterFrame>\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n"],"mappings":""}
@@ -1,4 +1,4 @@
1
- import type { FilterFieldType, FilterOperator, FilterOption, FilterValue } from './types';
1
+ import type { FilterFieldType, FilterOperator, FilterOption, FilterSummary, FilterValue } from './types';
2
2
  export interface FilterChipProps {
3
3
  /** A filter, read as a sentence; `add` is the quiet "+ Filter" that starts one (FilterMenu includes it). */
4
4
  kind?: 'filter' | 'add';
@@ -11,6 +11,8 @@ export interface FilterChipProps {
11
11
  operators?: FilterOperator[];
12
12
  /** The choices for enum, person and company. */
13
13
  options?: FilterOption[];
14
+ /** Your own wording for the chip, in place of field, operator and value: "Not archived", "Created in the last 7 days". */
15
+ summary?: FilterSummary;
14
16
  /** The live result count in the panel's footer, e.g. "120 people". */
15
17
  status?: string;
16
18
  /** The plural noun once more than two values are picked: "3 stages". */
@@ -49,6 +51,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
49
51
  "onUpdate:op"?: ((value: string | undefined) => any) | undefined;
50
52
  }>, {
51
53
  type: FilterFieldType;
54
+ summary: FilterSummary;
52
55
  icon: string;
53
56
  closeLabel: string;
54
57
  status: string;