@heyramzi/ui 2.22.1 → 2.24.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.
- package/COMPONENTS.md +3 -6
- package/dist/ai/confirmation/index.js +3 -6
- package/dist/ai/index.js +1 -5
- package/dist/canvas/editor/rebase.js +26 -2
- package/dist/charts/dashboard/index.js +1 -3
- package/dist/charts/primitives/chart-utils.js +14 -0
- package/dist/components/chip/index.js +1 -0
- package/dist/components/language-flag/flags.js +5 -0
- package/dist/components/language-flag/index.js +1 -0
- package/dist/components/list-page/index.js +1 -3
- package/dist/components/work-parts/index.js +1 -1
- package/dist/editor/doc-blocks.js +2 -1
- package/dist/editor/safe-url.js +26 -0
- package/dist/editor/serializer.js +10 -4
- package/dist/editor/slash-query.js +10 -0
- package/dist/effects/animated-beam/path-calculator.svelte.js +13 -6
- package/dist/lib/dates.js +41 -0
- package/dist/lib/panel.js +2 -2
- package/dist/lib/tone.js +13 -0
- package/dist/record/format.js +2 -18
- package/dist/record/logic.js +3 -6
- package/dist/status/index.js +1 -1
- package/dist/triage/index.js +4 -0
- package/dist/triage/types.js +12 -0
- package/dist/work/dates.js +2 -3
- package/dist/work/detail/detail-types.js +0 -8
- package/dist/work/docs/edited.js +8 -7
- package/dist/work/inbox/inbox-logic.js +5 -20
- package/dist/work/index.js +0 -2
- package/dist/work/time/duration.js +2 -3
- package/package.json +7 -21
- package/scripts/sync-shadcn.mjs +4 -6
- package/src/ai/confirmation/Confirmation.svelte +2 -2
- package/src/ai/confirmation/{ConfirmationRequest.svelte → ConfirmationState.svelte} +6 -3
- package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
- package/src/ai/confirmation/confirmation-parts.test.svelte +17 -0
- package/src/ai/confirmation/confirmation-parts.test.ts +23 -0
- package/src/ai/confirmation/index.ts +6 -21
- package/src/ai/index.ts +3 -13
- package/src/ai/typing-dots/TypingDots.svelte +32 -12
- package/src/ai/typing-dots/typing-dots.test.ts +36 -0
- package/src/animations/modal.css +4 -1
- package/src/canvas/editor/rebase.test.ts +37 -0
- package/src/canvas/editor/rebase.ts +33 -1
- package/src/canvas/ui/canvas-ui.css +44 -1
- package/src/charts/README.md +2 -2
- package/src/charts/area-chart.svelte +5 -12
- package/src/charts/bar-chart.svelte +6 -14
- package/src/charts/chart-tokens.css +19 -0
- package/src/charts/dashboard/data-table.svelte +21 -21
- package/src/charts/dashboard/index.ts +1 -3
- package/src/charts/dashboard/rating-chip.svelte +9 -11
- package/src/charts/dashboard/stat-tile.svelte +4 -5
- package/src/charts/donut-chart.svelte +1 -1
- package/src/charts/gauge-chart.svelte +1 -1
- package/src/charts/line-chart.svelte +5 -12
- package/src/charts/number-chart.svelte +1 -1
- package/src/charts/primitives/chart-utils.test.ts +17 -0
- package/src/charts/primitives/chart-utils.ts +16 -0
- package/src/charts/ring-chart.svelte +2 -2
- package/src/charts/sparkline.svelte +1 -1
- package/src/charts/stat-card.svelte +1 -1
- package/src/charts/time-series-chart.svelte +9 -5
- package/src/components/chip/count-chip.svelte +35 -0
- package/src/components/chip/index.ts +1 -0
- package/src/components/data-table/data-table.ts +1 -1
- package/src/components/data-table/row-context-menu.svelte +3 -4
- package/src/components/data-table/sortable-head.svelte +45 -10
- package/src/components/language-flag/flags.ts +5 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +3 -11
- package/src/components/list-page/empty-state.svelte +90 -12
- package/src/components/list-page/filter-popover.svelte +2 -5
- package/src/components/list-page/grouped-list.svelte +2 -4
- package/src/components/list-page/index.ts +1 -3
- package/src/components/section-header/section-header.svelte +2 -6
- package/src/components/segmented/segment.svelte +13 -19
- package/src/components/segmented/segmented-filter.svelte +1 -2
- package/src/components/table-view/table-view.svelte +31 -45
- package/src/components/table-view/table-view.test.ts +6 -3
- package/src/components/work-parts/group-head.svelte +1 -1
- package/src/components/work-parts/index.ts +1 -1
- package/src/editor/doc-blocks.ts +2 -1
- package/src/editor/editor.svelte +7 -5
- package/src/editor/safe-url.ts +32 -0
- package/src/editor/serializer.test.ts +58 -0
- package/src/editor/serializer.ts +9 -6
- package/src/editor/slash-query.test.ts +19 -0
- package/src/editor/slash-query.ts +11 -0
- package/src/effects/animated-beam/path-calculator.svelte.ts +19 -5
- package/src/effects/marquee/Marquee.css +6 -3
- package/src/effects/marquee/Marquee.svelte +6 -2
- package/src/inbox/thread-inbox.svelte +6 -4
- package/src/lib/dates.test.ts +63 -0
- package/src/lib/dates.ts +63 -0
- package/src/lib/panel.svelte +116 -0
- package/src/lib/panel.ts +2 -2
- package/src/lib/tone.ts +15 -0
- package/src/modules.test.ts +13 -1
- package/src/panel/side-panel.svelte +4 -4
- package/src/record/format.ts +3 -15
- package/src/record/logic.ts +2 -6
- package/src/record/record-stats.svelte +4 -3
- package/src/record/steps-rail.svelte +7 -4
- package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
- package/src/shadcn/button/button.svelte +1 -1
- package/src/shadcn/button/button.test.ts +2 -2
- package/src/shadcn/command/command-dialog.svelte +5 -10
- package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
- package/src/shadcn/input/input.svelte +2 -2
- package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
- package/src/status/index.ts +1 -1
- package/src/status/status-pill.svelte +2 -8
- package/src/thread/thread.svelte +3 -6
- package/src/tokens/craft.css +30 -0
- package/src/triage/README.md +28 -0
- package/src/triage/index.ts +10 -0
- package/src/triage/triage-plan.svelte +132 -0
- package/src/triage/triage-queue.svelte +48 -0
- package/src/triage/triage-task.svelte +95 -0
- package/src/triage/types.ts +35 -0
- package/src/work/README.md +0 -1
- package/src/work/dates.ts +3 -3
- package/src/work/demo.svelte +9 -1
- package/src/work/detail/detail-demo.svelte +30 -27
- package/src/work/detail/detail-types.ts +1 -29
- package/src/work/detail/task-detail.svelte +18 -17
- package/src/work/docs/doc-menu.svelte +2 -1
- package/src/work/docs/edited.ts +4 -7
- package/src/work/inbox/inbox-card.svelte +4 -9
- package/src/work/inbox/inbox-logic.ts +4 -16
- package/src/work/index.ts +1 -3
- package/src/work/kanban-column.svelte +5 -3
- package/src/work/time/duration.ts +2 -3
- package/src/work/time/time-chip.svelte +1 -3
- package/dist/ai/thinking-dots/index.js +0 -3
- package/dist/components/icon/index.js +0 -1
- package/dist/components/locale-switch/index.js +0 -1
- package/dist/icons/trades.js +0 -49
- package/dist/status/pill-tint.js +0 -13
- package/dist/work/presets/sales.js +0 -39
- package/src/ai/confirmation/ConfirmationAccepted.svelte +0 -23
- package/src/ai/confirmation/ConfirmationFailed.svelte +0 -23
- package/src/ai/confirmation/ConfirmationRejected.svelte +0 -23
- package/src/ai/thinking-dots/ThinkingDots.svelte +0 -42
- package/src/ai/thinking-dots/index.ts +0 -5
- package/src/charts/dashboard/dashboard-empty.svelte +0 -55
- package/src/charts/dashboard/data-panel.svelte +0 -81
- package/src/charts/dashboard/data-sort-head.svelte +0 -102
- package/src/components/icon/icon.svelte +0 -23
- package/src/components/icon/index.ts +0 -1
- package/src/components/list-page/detail-empty.svelte +0 -45
- package/src/components/list-page/detail-panel.svelte +0 -82
- package/src/components/list-page/rail-empty.svelte +0 -44
- package/src/components/locale-switch/index.ts +0 -1
- package/src/components/locale-switch/locale-switch.svelte +0 -59
- package/src/components/work-parts/count-chip.svelte +0 -20
- package/src/icons/TradeIcon.svelte +0 -16
- package/src/icons/trades.ts +0 -89
- package/src/status/pill-tint.ts +0 -16
- package/src/work/detail/task-property.svelte +0 -17
- package/src/work/detail/task-thread.svelte +0 -221
- package/src/work/presets/sales.ts +0 -16
|
@@ -3,22 +3,14 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
-
|
|
7
|
-
// =========================
|
|
8
|
-
// CONSTANTS
|
|
9
|
-
// =========================
|
|
10
|
-
// The host serves these files from /flags. Pass `flags` for another set or path.
|
|
11
|
-
const DEFAULT_FLAGS: Record<string, string> = {
|
|
12
|
-
en: "/flags/US.svg",
|
|
13
|
-
fr: "/flags/FR.svg",
|
|
14
|
-
};
|
|
6
|
+
import { LANGUAGE_FLAGS } from "./flags.js";
|
|
15
7
|
|
|
16
8
|
// =========================
|
|
17
9
|
// PROPS
|
|
18
10
|
// =========================
|
|
19
11
|
interface Props {
|
|
20
12
|
language: string;
|
|
21
|
-
/** Language code to image URL. */
|
|
13
|
+
/** Language code to image URL, for another set or path. */
|
|
22
14
|
flags?: Record<string, string>;
|
|
23
15
|
size?: "sm" | "md";
|
|
24
16
|
showLabel?: boolean;
|
|
@@ -27,7 +19,7 @@
|
|
|
27
19
|
|
|
28
20
|
let {
|
|
29
21
|
language,
|
|
30
|
-
flags =
|
|
22
|
+
flags = LANGUAGE_FLAGS,
|
|
31
23
|
size = "sm",
|
|
32
24
|
showLabel = false,
|
|
33
25
|
class: className,
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
5
6
|
import { Button } from "../../shadcn/button/index.js";
|
|
6
7
|
import * as Empty from "../../shadcn/empty/index.js";
|
|
8
|
+
import { cn } from "../../lib/utils.js";
|
|
7
9
|
import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
|
|
8
10
|
|
|
9
11
|
// =========================
|
|
@@ -12,25 +14,93 @@
|
|
|
12
14
|
interface Props {
|
|
13
15
|
icon?: PhosphorIcon;
|
|
14
16
|
iconWeight?: IconWeight;
|
|
17
|
+
/** Replaces the muted colour of a bare icon. Ignored by the tile, which stays foreground. */
|
|
18
|
+
iconClass?: string;
|
|
15
19
|
title: string;
|
|
16
20
|
description?: string;
|
|
17
21
|
actionLabel?: string;
|
|
18
22
|
actionHref?: string;
|
|
19
23
|
onAction?: () => void;
|
|
24
|
+
/** Several ways out, drawn in one row after the single action. */
|
|
25
|
+
actions?: Snippet;
|
|
26
|
+
/** `card` fills the white panel; `dashed` frames a whole dashboard section. */
|
|
27
|
+
surface?: "plain" | "card" | "dashed";
|
|
28
|
+
/** `sm` fits a 280px rail: a bare icon and one line of small type. */
|
|
29
|
+
size?: "default" | "sm";
|
|
30
|
+
class?: string;
|
|
20
31
|
}
|
|
21
32
|
|
|
33
|
+
/** A null `icon` draws the icon in a white tile. */
|
|
34
|
+
type Look = {
|
|
35
|
+
root: string;
|
|
36
|
+
icon: string | null;
|
|
37
|
+
header: string;
|
|
38
|
+
title: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
22
42
|
// =========================
|
|
23
43
|
// PROPS
|
|
24
44
|
// =========================
|
|
25
45
|
let {
|
|
26
46
|
icon: Icon,
|
|
27
47
|
iconWeight,
|
|
48
|
+
iconClass,
|
|
28
49
|
title,
|
|
29
50
|
description,
|
|
30
51
|
actionLabel,
|
|
31
52
|
actionHref,
|
|
32
53
|
onAction,
|
|
54
|
+
actions,
|
|
55
|
+
surface = "plain",
|
|
56
|
+
size = "default",
|
|
57
|
+
class: className,
|
|
33
58
|
}: Props = $props();
|
|
59
|
+
|
|
60
|
+
// =========================
|
|
61
|
+
// CONSTANTS
|
|
62
|
+
// =========================
|
|
63
|
+
// A white tile, not a brand tint: the accent marks the one action (DESIGN.md rule 1).
|
|
64
|
+
const PAGE: Look = {
|
|
65
|
+
root: "py-16",
|
|
66
|
+
icon: null,
|
|
67
|
+
header: "max-w-md",
|
|
68
|
+
title: "font-heading text-base font-semibold text-balance [overflow-wrap:anywhere]",
|
|
69
|
+
description: "",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const RAIL: Look = {
|
|
73
|
+
root: "gap-2 px-6 py-12",
|
|
74
|
+
icon: "size-6",
|
|
75
|
+
header: "max-w-none",
|
|
76
|
+
title: "",
|
|
77
|
+
description: "max-w-56 text-xs leading-relaxed",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// A dashed box stands in for a whole dashboard section, so it takes a heading's size.
|
|
81
|
+
const SECTION: Look = {
|
|
82
|
+
root: "p-10 text-wrap",
|
|
83
|
+
icon: "size-10",
|
|
84
|
+
header: "max-w-md gap-1.5",
|
|
85
|
+
title: "font-heading text-lg font-semibold tracking-normal",
|
|
86
|
+
description: "text-sm",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// A framed box sizes as a block, so a caller's `m-4` insets it instead of pushing it 32px wide.
|
|
90
|
+
const SURFACE: Record<NonNullable<Props["surface"]>, string> = {
|
|
91
|
+
plain: "",
|
|
92
|
+
card: "w-auto flex-initial bg-card",
|
|
93
|
+
dashed: "w-auto flex-initial border-2 border-border",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// =========================
|
|
97
|
+
// DERIVED
|
|
98
|
+
// =========================
|
|
99
|
+
let look = $derived(size === "sm" ? RAIL : surface === "dashed" ? SECTION : PAGE);
|
|
100
|
+
// Dashboard empties carry their own `t-enter`, and a rail empty flips on every filter keystroke.
|
|
101
|
+
let reveal = $derived(look === PAGE);
|
|
102
|
+
let heading = $derived(look === SECTION);
|
|
103
|
+
let hasAction = $derived(Boolean(actionLabel && (actionHref || onAction)));
|
|
34
104
|
</script>
|
|
35
105
|
|
|
36
106
|
<!-- =========================== -->
|
|
@@ -38,29 +108,37 @@
|
|
|
38
108
|
<!-- =========================== -->
|
|
39
109
|
<!-- transitions-dev texts reveal: the parts arrive in reading order, so the page reads as
|
|
40
110
|
loaded and empty. Reduced motion keeps the fade. -->
|
|
41
|
-
<Empty.Root class="t-empty
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
111
|
+
<Empty.Root class={cn(reveal && "t-empty", look.root, SURFACE[surface], className)}>
|
|
112
|
+
{#if Icon && look.icon}
|
|
113
|
+
<Icon
|
|
114
|
+
weight={iconWeight}
|
|
115
|
+
class={cn(look.icon, iconClass ?? "text-muted-foreground")}
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
/>
|
|
118
|
+
{/if}
|
|
119
|
+
<Empty.Header class={look.header}>
|
|
120
|
+
{#if Icon && !look.icon}
|
|
46
121
|
<Empty.Media class="mb-3 size-12 rounded-xl bg-card text-foreground shadow-card">
|
|
47
122
|
<Icon weight={iconWeight} class="size-6" />
|
|
48
123
|
</Empty.Media>
|
|
49
124
|
{/if}
|
|
50
|
-
<Empty.Title
|
|
51
|
-
|
|
125
|
+
<Empty.Title
|
|
126
|
+
class={look.title}
|
|
127
|
+
role={heading ? "heading" : undefined}
|
|
128
|
+
aria-level={heading ? 2 : undefined}>{title}</Empty.Title
|
|
52
129
|
>
|
|
53
130
|
{#if description}
|
|
54
|
-
<Empty.Description>{description}</Empty.Description>
|
|
131
|
+
<Empty.Description class={look.description}>{description}</Empty.Description>
|
|
55
132
|
{/if}
|
|
56
133
|
</Empty.Header>
|
|
57
|
-
{#if
|
|
58
|
-
<Empty.Content>
|
|
59
|
-
{#if actionHref}
|
|
134
|
+
{#if hasAction || actions}
|
|
135
|
+
<Empty.Content class="max-w-md flex-row flex-wrap justify-center gap-2">
|
|
136
|
+
{#if hasAction && actionHref}
|
|
60
137
|
<Button href={actionHref}>{actionLabel}</Button>
|
|
61
|
-
{:else}
|
|
138
|
+
{:else if hasAction}
|
|
62
139
|
<Button onclick={onAction}>{actionLabel}</Button>
|
|
63
140
|
{/if}
|
|
141
|
+
{@render actions?.()}
|
|
64
142
|
</Empty.Content>
|
|
65
143
|
{/if}
|
|
66
144
|
</Empty.Root>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
9
|
import { Button } from "../../shadcn/button/index.js";
|
|
10
10
|
import { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
11
|
+
import CountChip from "../chip/count-chip.svelte";
|
|
11
12
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
12
13
|
|
|
13
14
|
// phosphor-svelte v2 (resolved here) and v3 (what consumers run) differ only in
|
|
@@ -70,11 +71,7 @@
|
|
|
70
71
|
{@render trailing()}
|
|
71
72
|
{/if}
|
|
72
73
|
{#if hasCount}
|
|
73
|
-
<
|
|
74
|
-
class="ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-brand/12 px-1 text-xs leading-none font-semibold text-brand tabular-nums dark:text-brand-light"
|
|
75
|
-
>
|
|
76
|
-
{count.toLocaleString()}
|
|
77
|
-
</span>
|
|
74
|
+
<CountChip {count} size="sm" tone="brand" class="ml-0.5" />
|
|
78
75
|
{/if}
|
|
79
76
|
</Popover.Trigger>
|
|
80
77
|
<Popover.Content class={cn("p-2", width, contentClass)} {align}>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { SvelteSet } from "svelte/reactivity";
|
|
7
7
|
import { CaretDown, Plus } from "phosphor-svelte";
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import CountChip from "../chip/count-chip.svelte";
|
|
9
10
|
|
|
10
11
|
// =========================
|
|
11
12
|
// TYPES
|
|
@@ -110,10 +111,7 @@
|
|
|
110
111
|
/>
|
|
111
112
|
{@render header(group, { collapsed: isCollapsed })}
|
|
112
113
|
{#if getCount}
|
|
113
|
-
<
|
|
114
|
-
class="rounded-full bg-foreground/6 px-1.5 text-xs leading-5 font-medium text-muted-foreground tabular-nums"
|
|
115
|
-
>{getCount(group)}</span
|
|
116
|
-
>
|
|
114
|
+
<CountChip count={getCount(group)} />
|
|
117
115
|
{/if}
|
|
118
116
|
</button>
|
|
119
117
|
{#if actions}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { default as DetailEmpty } from "./detail-empty.svelte";
|
|
2
1
|
export { default as DetailHeader } from "./detail-header.svelte";
|
|
3
|
-
export { default as DetailPanel } from "
|
|
2
|
+
export { default as DetailPanel } from "../../lib/panel.svelte";
|
|
4
3
|
export { default as DetailRow } from "./detail-row.svelte";
|
|
5
4
|
export { default as DetailStack } from "./detail-stack.svelte";
|
|
6
5
|
export { default as EmptyState } from "./empty-state.svelte";
|
|
@@ -9,7 +8,6 @@ export { default as PrevNext } from "./prev-next.svelte";
|
|
|
9
8
|
export { stepTo } from "./step.js";
|
|
10
9
|
export { default as NoticeBar } from "./notice-bar.svelte";
|
|
11
10
|
export { default as RailBody } from "./rail-body.svelte";
|
|
12
|
-
export { default as RailEmpty } from "./rail-empty.svelte";
|
|
13
11
|
export { default as RailHead } from "./rail-head.svelte";
|
|
14
12
|
export { default as RailRow } from "./rail-row.svelte";
|
|
15
13
|
export { default as SectionLabel } from "../section-header/section-label.svelte";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// =========================
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import SectionLabel from "./section-label.svelte";
|
|
7
|
+
import CountChip from "../chip/count-chip.svelte";
|
|
7
8
|
|
|
8
9
|
// =========================
|
|
9
10
|
// TYPES
|
|
@@ -36,12 +37,7 @@
|
|
|
36
37
|
<svelte:element this={`h${level}`} class="flex items-center gap-2">
|
|
37
38
|
<SectionLabel class="min-w-0 [overflow-wrap:anywhere]">{title}</SectionLabel>
|
|
38
39
|
{#if count !== undefined}
|
|
39
|
-
<
|
|
40
|
-
class="shrink-0 rounded-full bg-foreground/6 px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
|
|
41
|
-
aria-label={`${count.toLocaleString()} ${countLabel}`}
|
|
42
|
-
>
|
|
43
|
-
{count.toLocaleString()}
|
|
44
|
-
</span>
|
|
40
|
+
<CountChip {count} size="sm" aria-label={`${count.toLocaleString()} ${countLabel}`} />
|
|
45
41
|
{/if}
|
|
46
42
|
</svelte:element>
|
|
47
43
|
{#if description}
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
// IMPORTS
|
|
31
31
|
// =========================
|
|
32
32
|
import { cn } from "../../lib/utils.js";
|
|
33
|
-
import { Icon } from "../icon/index.js";
|
|
34
33
|
import { Mark } from "../mark/index.js";
|
|
34
|
+
import CountChip from "../chip/count-chip.svelte";
|
|
35
35
|
|
|
36
36
|
// =========================
|
|
37
37
|
// TYPES
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
// =========================
|
|
48
48
|
// PROPS
|
|
49
49
|
// =========================
|
|
50
|
-
let { label, count, icon, image, done = false, badge, active = false }: Props = $props();
|
|
50
|
+
let { label, count, icon: Icon, image, done = false, badge, active = false }: Props = $props();
|
|
51
51
|
|
|
52
52
|
// =========================
|
|
53
53
|
// DERIVED
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
auto && "group-data-[state=active]/segment:opacity-100",
|
|
70
70
|
)}
|
|
71
71
|
/>
|
|
72
|
-
{:else if
|
|
73
|
-
<Icon
|
|
72
|
+
{:else if Icon}
|
|
73
|
+
<Icon size={16} class="shrink-0" />
|
|
74
74
|
{/if}
|
|
75
75
|
|
|
76
76
|
{label}
|
|
@@ -80,21 +80,15 @@
|
|
|
80
80
|
{/if}
|
|
81
81
|
|
|
82
82
|
{#if count != null}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
auto &&
|
|
93
|
-
"group-data-[state=active]/segment:bg-brand/14 group-data-[state=active]/segment:text-brand",
|
|
94
|
-
)}
|
|
95
|
-
>
|
|
96
|
-
{count.toLocaleString()}
|
|
97
|
-
</span>
|
|
83
|
+
<!-- WHY 16px, not 18: inside a 22px segment, 18 read as a chip nested in the pill. -->
|
|
84
|
+
<CountChip
|
|
85
|
+
{count}
|
|
86
|
+
size="sm"
|
|
87
|
+
tone={on ? "brand" : "neutral"}
|
|
88
|
+
class={auto
|
|
89
|
+
? "group-data-[state=active]/segment:bg-brand/12 group-data-[state=active]/segment:text-brand dark:group-data-[state=active]/segment:text-brand-light"
|
|
90
|
+
: undefined}
|
|
91
|
+
/>
|
|
98
92
|
{:else if done}
|
|
99
93
|
<span
|
|
100
94
|
class={cn(
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
splitSegments,
|
|
14
14
|
} from "./collapse.js";
|
|
15
15
|
import { segmentedTriggerClass } from "./segmented.js";
|
|
16
|
-
import { Icon } from "../icon/index.js";
|
|
17
16
|
import { Mark } from "../mark/index.js";
|
|
18
17
|
import { CaretDown } from "phosphor-svelte";
|
|
19
18
|
import { Picker } from "../picker/index.js";
|
|
@@ -162,7 +161,7 @@
|
|
|
162
161
|
)}
|
|
163
162
|
/>
|
|
164
163
|
{:else if markOnly && option.icon}
|
|
165
|
-
<
|
|
164
|
+
<option.icon size={16} class="shrink-0" />
|
|
166
165
|
{:else}
|
|
167
166
|
<Segment
|
|
168
167
|
label={option.label}
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
// =========================
|
|
12
12
|
// IMPORTS
|
|
13
13
|
// =========================
|
|
14
|
-
import { CaretDown
|
|
14
|
+
import { CaretDown } from "phosphor-svelte";
|
|
15
15
|
import { cn } from "../../lib/utils.js";
|
|
16
16
|
import * as Table from "../../shadcn/table/index.js";
|
|
17
17
|
import * as ContextMenu from "../../shadcn/context-menu/index.js";
|
|
18
18
|
import { Checkbox } from "../../shadcn/checkbox/index.js";
|
|
19
|
-
import { TableSkeleton } from "../data-table/index.js";
|
|
19
|
+
import { SortableHead, TableSkeleton } from "../data-table/index.js";
|
|
20
20
|
import { toggleSort, type SortState } from "../data-table/data-table.js";
|
|
21
21
|
|
|
22
22
|
// =========================
|
|
@@ -174,12 +174,6 @@
|
|
|
174
174
|
return value === null || value === undefined ? "" : String(value);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function ariaSort(column: TableColumn<Row>): "ascending" | "descending" | "none" | undefined {
|
|
178
|
-
if (!column.sortable) return undefined;
|
|
179
|
-
if (sort?.key !== column.key) return "none";
|
|
180
|
-
return sort.direction === "asc" ? "ascending" : "descending";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
177
|
function toggleRow(row: Row, on: boolean) {
|
|
184
178
|
const next = new Set(selected);
|
|
185
179
|
if (on) next.add(rowKey(row));
|
|
@@ -358,43 +352,35 @@
|
|
|
358
352
|
</Table.Head>
|
|
359
353
|
{/if}
|
|
360
354
|
{#each columns as column (column.key)}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
{/if}
|
|
391
|
-
</button>
|
|
392
|
-
{:else if head}
|
|
393
|
-
{@render head(column)}
|
|
394
|
-
{:else}
|
|
395
|
-
{column.label}
|
|
396
|
-
{/if}
|
|
397
|
-
</Table.Head>
|
|
355
|
+
{@const headClass = cn(
|
|
356
|
+
"whitespace-normal",
|
|
357
|
+
column.numeric && "text-end",
|
|
358
|
+
column.hideBelow && HIDE[column.hideBelow],
|
|
359
|
+
column.sticky && "bg-card sticky left-0 z-20",
|
|
360
|
+
column.headClass ?? column.class,
|
|
361
|
+
)}
|
|
362
|
+
{@const headStyle = column.width ? `width: ${column.width}` : undefined}
|
|
363
|
+
{#if column.sortable}
|
|
364
|
+
<!-- A sticky head keeps its card under the hover tint, or rows show through it. -->
|
|
365
|
+
<SortableHead
|
|
366
|
+
key={column.key as keyof Row & string}
|
|
367
|
+
{sort}
|
|
368
|
+
onSort={(key) => (sort = toggleSort(sort, key))}
|
|
369
|
+
numeric={column.numeric}
|
|
370
|
+
class={cn(
|
|
371
|
+
headClass,
|
|
372
|
+
column.sticky &&
|
|
373
|
+
"hover:bg-[color-mix(in_oklab,var(--color-foreground)_4%,var(--color-card))]",
|
|
374
|
+
)}
|
|
375
|
+
style={headStyle}
|
|
376
|
+
>
|
|
377
|
+
{#if head}{@render head(column)}{:else}{column.label}{/if}
|
|
378
|
+
</SortableHead>
|
|
379
|
+
{:else}
|
|
380
|
+
<Table.Head class={headClass} style={headStyle}>
|
|
381
|
+
{#if head}{@render head(column)}{:else}{column.label}{/if}
|
|
382
|
+
</Table.Head>
|
|
383
|
+
{/if}
|
|
398
384
|
{/each}
|
|
399
385
|
{#if rowHref}
|
|
400
386
|
<th class="w-0 p-0"><span class="sr-only">Open</span></th>
|
|
@@ -64,14 +64,17 @@ describe("TableView extensions", () => {
|
|
|
64
64
|
{ key: "total", label: "Total", numeric: true, sortable: true },
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
-
it("sorts from a head
|
|
67
|
+
it("sorts from a head, by click or Enter, and sets aria-sort", () => {
|
|
68
68
|
const { target, done } = render({ columns: sortable, sort: { key: "name", direction: "asc" } });
|
|
69
|
-
const heads = target.querySelectorAll("thead th");
|
|
69
|
+
const heads = target.querySelectorAll<HTMLElement>("thead th");
|
|
70
70
|
expect(heads[0].getAttribute("aria-sort")).toBe("ascending");
|
|
71
71
|
expect(heads[1].getAttribute("aria-sort")).toBe("none");
|
|
72
|
-
|
|
72
|
+
heads[1].click();
|
|
73
73
|
flushSync();
|
|
74
74
|
expect(target.querySelectorAll("thead th")[1].getAttribute("aria-sort")).toBe("ascending");
|
|
75
|
+
heads[1].dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
76
|
+
flushSync();
|
|
77
|
+
expect(target.querySelectorAll("thead th")[1].getAttribute("aria-sort")).toBe("descending");
|
|
75
78
|
done();
|
|
76
79
|
});
|
|
77
80
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Plus } from "phosphor-svelte";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
5
|
import { Button } from "../../shadcn/button/index.js";
|
|
6
|
-
import CountChip from "
|
|
6
|
+
import CountChip from "../chip/count-chip.svelte";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
label: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { default as CountChip } from "
|
|
1
|
+
export { default as CountChip } from "../chip/count-chip.svelte";
|
|
2
2
|
export { default as GroupHead } from "./group-head.svelte";
|
|
3
3
|
export { default as PropertyRow } from "./property-row.svelte";
|
|
4
4
|
export { default as WorkCard } from "./work-card.svelte";
|
package/src/editor/doc-blocks.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Extension } from "@tiptap/core";
|
|
6
6
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
7
7
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
8
|
+
import { copyText } from "../components/copy/clipboard.js";
|
|
8
9
|
import { anchorIds } from "./anchors.js";
|
|
9
10
|
|
|
10
11
|
const LINK_ICON = `<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M240 88.23a54.43 54.43 0 0 1-16 37L189.25 160a54.27 54.27 0 0 1-38.63 16h-.05A54.63 54.63 0 0 1 96 119.84a8 8 0 0 1 16 .45A38.62 38.62 0 0 0 150.58 160a38.4 38.4 0 0 0 27.31-11.31l34.75-34.75a38.63 38.63 0 0 0-54.63-54.63l-11 11A8 8 0 0 1 135.7 59l11-11a54.65 54.65 0 0 1 77.3 0A54.86 54.86 0 0 1 240 88.23ZM109 185.66l-11 11A38.41 38.41 0 0 1 70.6 208a38.63 38.63 0 0 1-27.29-65.94L78 107.31a38.63 38.63 0 0 1 54.63 0 8 8 0 0 0 11.31-11.31 54.65 54.65 0 0 0-77.3 0L32 130.75A54.62 54.62 0 0 0 70.56 224a54.28 54.28 0 0 0 38.64-16l11-11a8 8 0 0 0-11.2-11.34Z"/></svg>`;
|
|
@@ -59,7 +60,7 @@ export const HeadingAnchors = Extension.create<HeadingAnchorsOptions>({
|
|
|
59
60
|
button.addEventListener("click", () => {
|
|
60
61
|
const { origin, pathname, search } = window.location;
|
|
61
62
|
const url = `${origin}${pathname}${search}#${id}`;
|
|
62
|
-
void
|
|
63
|
+
void copyText(url);
|
|
63
64
|
button.dataset.copied = "true";
|
|
64
65
|
setTimeout(() => delete button.dataset.copied, 1400);
|
|
65
66
|
options.oncopy?.(url, id);
|
package/src/editor/editor.svelte
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import "./doc-page.css";
|
|
9
9
|
import { buildExtensions } from "./extensions.js";
|
|
10
10
|
import { buildSlashItems, type SlashItem } from "./slash-items.js";
|
|
11
|
+
import { SLASH_WINDOW, slashQueryAt } from "./slash-query.js";
|
|
11
12
|
import { parseYoutubeUrl } from "./youtube-url.js";
|
|
12
13
|
import EditorToolbar from "./editor-toolbar.svelte";
|
|
13
14
|
import SlashMenu from "./slash-menu.svelte";
|
|
@@ -238,7 +239,8 @@
|
|
|
238
239
|
content: content ?? { type: "doc", content: [{ type: "paragraph" }] },
|
|
239
240
|
editorProps: {
|
|
240
241
|
handleKeyDown: (_view, event) => {
|
|
241
|
-
|
|
242
|
+
// No items means no menu on screen, so Enter and the arrows stay the editor's.
|
|
243
|
+
if (showSlashMenu && slashItems.length > 0) {
|
|
242
244
|
if (event.key === "ArrowDown") {
|
|
243
245
|
event.preventDefault();
|
|
244
246
|
slashSelectedIndex = (slashSelectedIndex + 1) % slashItems.length;
|
|
@@ -294,11 +296,11 @@
|
|
|
294
296
|
onchange?.(e.getJSON());
|
|
295
297
|
|
|
296
298
|
const { from } = e.state.selection;
|
|
297
|
-
const textBefore = e.state.doc.textBetween(Math.max(0, from -
|
|
298
|
-
const
|
|
299
|
+
const textBefore = e.state.doc.textBetween(Math.max(0, from - SLASH_WINDOW), from, "\n");
|
|
300
|
+
const query = slashQueryAt(textBefore);
|
|
299
301
|
|
|
300
|
-
if (
|
|
301
|
-
slashQuery =
|
|
302
|
+
if (query !== null) {
|
|
303
|
+
slashQuery = query;
|
|
302
304
|
slashMenuPos = getCaretCoords();
|
|
303
305
|
showSlashMenu = true;
|
|
304
306
|
} else if (showSlashMenu) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const SAFE_SCHEME = /^(?:https?:|mailto:|tel:)/i;
|
|
2
|
+
const ANY_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
3
|
+
// WHY no SVG: an SVG can carry script, and pasted images are stored as these data: URLs.
|
|
4
|
+
const SAFE_IMAGE_DATA = /^data:image\/(?:png|jpe?g|gif|webp|avif);base64,/i;
|
|
5
|
+
|
|
6
|
+
// WHY: browsers drop tabs, newlines and control bytes from a URL, so "java\tscript:" runs as javascript:.
|
|
7
|
+
function clean(raw: string): string {
|
|
8
|
+
return [...raw]
|
|
9
|
+
.filter((c) => c.charCodeAt(0) > 31 && c.charCodeAt(0) !== 127)
|
|
10
|
+
.join("")
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The URL when it's safe to put in an href, else null.
|
|
15
|
+
export function safeUrl(raw: string | null | undefined): string | null {
|
|
16
|
+
if (!raw) return null;
|
|
17
|
+
const href = clean(raw);
|
|
18
|
+
// Relative URLs and same-page anchors carry no scheme, so they are safe.
|
|
19
|
+
if (href.startsWith("#") || href.startsWith("/") || href.startsWith(".")) return href;
|
|
20
|
+
if (SAFE_SCHEME.test(href)) return href;
|
|
21
|
+
if (!ANY_SCHEME.test(href)) return href;
|
|
22
|
+
return null; // javascript:, data:, vbscript:, ...
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The URL when it's safe to put in an img src, else null.
|
|
26
|
+
export function safeImageUrl(raw: string | null | undefined): string | null {
|
|
27
|
+
const url = safeUrl(raw);
|
|
28
|
+
if (url !== null) return url;
|
|
29
|
+
if (!raw) return null;
|
|
30
|
+
const src = clean(raw);
|
|
31
|
+
return SAFE_IMAGE_DATA.test(src) ? src : null;
|
|
32
|
+
}
|
|
@@ -87,6 +87,64 @@ describe("renderTiptapContent", () => {
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
function linkDoc(href: string) {
|
|
91
|
+
return {
|
|
92
|
+
type: "doc",
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "paragraph",
|
|
96
|
+
content: [{ type: "text", text: "click", marks: [{ type: "link", attrs: { href } }] }],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function imageDoc(src: string) {
|
|
103
|
+
return { type: "doc", content: [{ type: "image", attrs: { src } }] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("unsafe URLs", () => {
|
|
107
|
+
it("keeps the text and drops a javascript: href", () => {
|
|
108
|
+
const html = renderTiptapContent(linkDoc("javascript:alert(1)"));
|
|
109
|
+
expect(html).toContain("click");
|
|
110
|
+
expect(html).not.toContain("javascript:");
|
|
111
|
+
expect(html).not.toContain("href=");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("drops a javascript: href hidden by a tab inside the scheme", () => {
|
|
115
|
+
const html = renderTiptapContent(linkDoc("java\tscript:alert(1)"));
|
|
116
|
+
expect(html).toContain("click");
|
|
117
|
+
expect(html).not.toContain("javascript:");
|
|
118
|
+
expect(html).not.toContain("href=");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it.each(["data:text/html,x", "vbscript:x"])("drops the %s href", (href) => {
|
|
122
|
+
expect(renderTiptapContent(linkDoc(href))).not.toContain("href=");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each(["https://example.com/a?b=1", "/docs", "#top", "mailto:a@example.com"])(
|
|
126
|
+
"keeps the %s href",
|
|
127
|
+
(href) => {
|
|
128
|
+
expect(renderTiptapContent(linkDoc(href))).toContain(`href="${href}"`);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
it("drops an image with a javascript: src", () => {
|
|
133
|
+
expect(renderTiptapContent(imageDoc("javascript:alert(1)"))).not.toContain("<img");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it.each(["https://example.com/a.png", "data:image/png;base64,AAAA"])(
|
|
137
|
+
"keeps an image with the %s src",
|
|
138
|
+
(src) => {
|
|
139
|
+
expect(renderTiptapContent(imageDoc(src))).toContain("<img");
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
it("drops an image with an SVG data: src", () => {
|
|
144
|
+
expect(renderTiptapContent(imageDoc("data:image/svg+xml;base64,AAAA"))).not.toContain("<img");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
90
148
|
describe("youtube node", () => {
|
|
91
149
|
function youtubeDoc(attrs: Record<string, unknown>) {
|
|
92
150
|
return { type: "doc", content: [{ type: "youtube", attrs }] };
|