@lovett/ui 0.0.11 → 0.2.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/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +5574 -57
- package/dist/index.js +21650 -11206
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1570 -0
- package/dist/tokens.css +112 -0
- package/package.json +8 -1
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +307 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +156 -2
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/sortable.test.tsx +394 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +369 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +611 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +301 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +290 -19
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +303 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +43 -1
- package/src/lib/layer-stack.ts +30 -3
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/modal.tsx +42 -18
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/sortable.tsx +520 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1570 -0
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +742 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +967 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +232 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +112 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lovett/ui — the item-detail surface.
|
|
3
|
+
*
|
|
4
|
+
* `docs/design-briefs/tasks-item-detail-full.md`. A frame with three modes, a
|
|
5
|
+
* slotted header, the ellipsis menu, the Notion-style field rows, and the
|
|
6
|
+
* activity pane. Entity-agnostic by construction: nothing here fetches, and
|
|
7
|
+
* nothing here knows what a task is. The lens supplies the data and the
|
|
8
|
+
* behaviour; this package supplies the surface.
|
|
9
|
+
*
|
|
10
|
+
* `MonthCalendar` is deliberately NOT exported — it is the date row's editor,
|
|
11
|
+
* not a general calendar, and promoting it would freeze a shape that has had
|
|
12
|
+
* exactly one consumer.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { DetailSurface, type DetailSurfaceProps } from './detail-surface'
|
|
16
|
+
export { DetailHeader, type DetailHeaderProps } from './detail-header'
|
|
17
|
+
export {
|
|
18
|
+
DetailMenu,
|
|
19
|
+
type DetailMenuProps,
|
|
20
|
+
type DetailMenuAction,
|
|
21
|
+
type DetailMenuItem,
|
|
22
|
+
} from './detail-menu'
|
|
23
|
+
export { DetailDivider, type DetailDividerProps } from './detail-divider'
|
|
24
|
+
|
|
25
|
+
export { FieldList, useFieldRowVisible, type FieldListProps } from './field-list'
|
|
26
|
+
export {
|
|
27
|
+
FieldRow,
|
|
28
|
+
FieldValueButton,
|
|
29
|
+
type FieldRowProps,
|
|
30
|
+
type FieldValueButtonProps,
|
|
31
|
+
type FieldRowTextProps,
|
|
32
|
+
type FieldRowDateProps,
|
|
33
|
+
type FieldRowSelectProps,
|
|
34
|
+
type FieldRowPeopleProps,
|
|
35
|
+
type FieldRowTagsProps,
|
|
36
|
+
} from './field-row'
|
|
37
|
+
|
|
38
|
+
export { ActivityPane, type ActivityPaneProps, type ActivityTab } from './activity-pane'
|
|
39
|
+
|
|
40
|
+
/** Date values are `YYYY-MM-DD` / `YYYY-MM-DDTHH:mm`, parsed as LOCAL. */
|
|
41
|
+
export { formatDateDisplay, parseDateValue, type DateParts } from './calendar'
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
DETAIL_COLLAPSE_VALUES,
|
|
45
|
+
DETAIL_MODES,
|
|
46
|
+
DETAIL_MODE_LABELS,
|
|
47
|
+
DETAIL_PANEL_DEFAULT_SIZE,
|
|
48
|
+
FIELD_FILTERS,
|
|
49
|
+
FIELD_FILTER_LABELS,
|
|
50
|
+
detailPaneSide,
|
|
51
|
+
type ActivityEntry,
|
|
52
|
+
type DetailCollapse,
|
|
53
|
+
type DetailMode,
|
|
54
|
+
type DetailPaneSide,
|
|
55
|
+
type FieldFilter,
|
|
56
|
+
type FieldOption,
|
|
57
|
+
type FieldPerson,
|
|
58
|
+
} from './types'
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SegmentedChoice — a one-of-N choice, internal to the detail surface.
|
|
3
|
+
*
|
|
4
|
+
* NOT `SegmentedPill`, and the reason is a11y rather than taste: that
|
|
5
|
+
* primitive renders `role="tablist"` with `role="tab"` segments, which is
|
|
6
|
+
* correct for a view switcher wired to tab panels and wrong for the two
|
|
7
|
+
* choices here — the mode row in the ellipsis menu, and the All / Filled /
|
|
8
|
+
* Empty filter chips. Neither controls a tab panel, and a screen reader that
|
|
9
|
+
* announces "tab 2 of 3" for a filter is being told something untrue.
|
|
10
|
+
* `role="group"` + `aria-pressed` says what these actually are.
|
|
11
|
+
*
|
|
12
|
+
* The selected state is the one CLAUDE.md §2 mandates: an opaque step UP
|
|
13
|
+
* (`--surface-card` on a `--surface-inset` track) with an accent border and
|
|
14
|
+
* `--shadow-sm`. Never an alpha tint on a translucent track — two alpha
|
|
15
|
+
* layers composite to a raised element that renders DARKER than the well it
|
|
16
|
+
* sits in.
|
|
17
|
+
*
|
|
18
|
+
* Not exported from the package barrel; it is a shape, not a contract.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ReactNode } from 'react'
|
|
22
|
+
import { cn } from '../lib/utils'
|
|
23
|
+
|
|
24
|
+
export interface SegmentedChoiceItem<Id extends string> {
|
|
25
|
+
readonly id: Id
|
|
26
|
+
/** Visible text, or the accessible name when `icon` carries the meaning. */
|
|
27
|
+
readonly label: string
|
|
28
|
+
readonly icon?: ReactNode | undefined
|
|
29
|
+
/** Render the icon alone and keep `label` as the accessible name. */
|
|
30
|
+
readonly iconOnly?: boolean | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SegmentedChoiceProps<Id extends string> {
|
|
34
|
+
items: readonly SegmentedChoiceItem<Id>[]
|
|
35
|
+
value: Id
|
|
36
|
+
onChange: (next: Id) => void
|
|
37
|
+
/** Names the group. Required — a bare row of buttons has no name. */
|
|
38
|
+
ariaLabel: string
|
|
39
|
+
/** Stretch the segments to fill the track. Default false. */
|
|
40
|
+
stretch?: boolean | undefined
|
|
41
|
+
className?: string | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function SegmentedChoice<Id extends string>({
|
|
45
|
+
items,
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
ariaLabel,
|
|
49
|
+
stretch = false,
|
|
50
|
+
className,
|
|
51
|
+
}: SegmentedChoiceProps<Id>) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
role="group"
|
|
55
|
+
aria-label={ariaLabel}
|
|
56
|
+
data-slot="segmented-choice"
|
|
57
|
+
className={cn('inline-flex items-center gap-[var(--space-1)] border p-[var(--space-1)]', className)}
|
|
58
|
+
style={{
|
|
59
|
+
borderRadius: 'var(--radius-md)',
|
|
60
|
+
borderColor: 'rgb(var(--border))',
|
|
61
|
+
background: 'rgb(var(--surface-inset))',
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{items.map((item) => {
|
|
65
|
+
const active = item.id === value
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
key={item.id}
|
|
69
|
+
type="button"
|
|
70
|
+
data-choice={item.id}
|
|
71
|
+
aria-pressed={active}
|
|
72
|
+
{...(item.iconOnly ? { 'aria-label': item.label } : {})}
|
|
73
|
+
onClick={() => onChange(item.id)}
|
|
74
|
+
className={cn(
|
|
75
|
+
'inline-flex h-[26px] items-center justify-center gap-[var(--space-1)]',
|
|
76
|
+
'px-[var(--space-2)] text-[12px] font-semibold',
|
|
77
|
+
stretch && 'flex-1',
|
|
78
|
+
'transition-[background-color,color,border-color,box-shadow] duration-[var(--dur-fast)] ease-[var(--ease-out)]',
|
|
79
|
+
'motion-reduce:transition-none',
|
|
80
|
+
'focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]',
|
|
81
|
+
active
|
|
82
|
+
? 'border border-[rgb(var(--accent))] bg-[rgb(var(--surface-card))] text-[rgb(var(--accent-ink))] [box-shadow:var(--shadow-sm)]'
|
|
83
|
+
: 'border border-transparent text-[rgb(var(--text-tertiary))] hover:text-[rgb(var(--foreground))]',
|
|
84
|
+
)}
|
|
85
|
+
style={{ borderRadius: 'var(--radius-sm)' }}
|
|
86
|
+
>
|
|
87
|
+
{item.icon}
|
|
88
|
+
{item.iconOnly ? null : item.label}
|
|
89
|
+
</button>
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item detail — the shared vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* `docs/design-briefs/tasks-item-detail-full.md` §1. Three modes, and the
|
|
5
|
+
* orientation of the activity pane FOLLOWS the mode rather than being a second
|
|
6
|
+
* setting that can disagree with it: side-by-side in the two centred modes,
|
|
7
|
+
* stacked in the side panel. `detailPaneSide()` is the only place that mapping
|
|
8
|
+
* exists, so a host cannot ship a `full` detail with a bottom pane.
|
|
9
|
+
*
|
|
10
|
+
* Nothing in this module renders. It is the type surface the lens imports.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ReactNode } from 'react'
|
|
14
|
+
|
|
15
|
+
/** §1 — `full` is the default; the host persists the choice. */
|
|
16
|
+
export type DetailMode = 'full' | 'modal' | 'panel'
|
|
17
|
+
|
|
18
|
+
/** Menu order: the three mode icons read left to right in this order. */
|
|
19
|
+
export const DETAIL_MODES: readonly DetailMode[] = ['full', 'modal', 'panel']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentence case, and each names what happens rather than what it is
|
|
23
|
+
* (interface standards §7). These are accessible names for icon-only
|
|
24
|
+
* controls, so they have to survive being read on their own.
|
|
25
|
+
*/
|
|
26
|
+
export const DETAIL_MODE_LABELS: Readonly<Record<DetailMode, string>> = {
|
|
27
|
+
full: 'Open full screen',
|
|
28
|
+
modal: 'Open centred',
|
|
29
|
+
panel: 'Open in side panel',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `panel` mode's default width, px — what the mode's `max-inline-size` was
|
|
34
|
+
* before its leading edge became draggable, so a host with nothing stored
|
|
35
|
+
* opens exactly where it always did, and `Home` / double-click on the edge
|
|
36
|
+
* come back to it.
|
|
37
|
+
*
|
|
38
|
+
* Exported because the HOST persists the width. A store that hard-codes its
|
|
39
|
+
* own copy of the default is one release away from disagreeing with the
|
|
40
|
+
* surface it is driving.
|
|
41
|
+
*/
|
|
42
|
+
export const DETAIL_PANEL_DEFAULT_SIZE = 560
|
|
43
|
+
|
|
44
|
+
/** Where the activity pane sits relative to the fields. */
|
|
45
|
+
export type DetailPaneSide = 'side' | 'bottom'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* §1 — "Orientation follows the mode." `full` and `modal` put activity to the
|
|
49
|
+
* inline end with a vertical divider; `panel` puts it below with a horizontal
|
|
50
|
+
* one.
|
|
51
|
+
*/
|
|
52
|
+
export function detailPaneSide(mode: DetailMode): DetailPaneSide {
|
|
53
|
+
return mode === 'panel' ? 'bottom' : 'side'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Which pane of the split is collapsed, if either.
|
|
58
|
+
*
|
|
59
|
+
* A THIRD state rather than two booleans: "both collapsed" is not a layout,
|
|
60
|
+
* and a pair of flags is one bad write away from expressing it. `fields` and
|
|
61
|
+
* `aside` name the pane that is HIDDEN, not the one that is showing, because
|
|
62
|
+
* that is what the restore affordance has to say out loud.
|
|
63
|
+
*
|
|
64
|
+
* It is per-ORIENTATION for the same reason the pane's size is: a host that
|
|
65
|
+
* collapsed the activity pane beside the fields has said nothing about
|
|
66
|
+
* whether it wants it collapsed below them.
|
|
67
|
+
*/
|
|
68
|
+
export type DetailCollapse = 'none' | 'fields' | 'aside'
|
|
69
|
+
|
|
70
|
+
/** For a host persisting the choice — the allow-list its store validates against. */
|
|
71
|
+
export const DETAIL_COLLAPSE_VALUES: readonly DetailCollapse[] = [
|
|
72
|
+
'none',
|
|
73
|
+
'fields',
|
|
74
|
+
'aside',
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
/** The chips above the field list. */
|
|
78
|
+
export type FieldFilter = 'all' | 'filled' | 'empty'
|
|
79
|
+
|
|
80
|
+
export const FIELD_FILTERS: readonly FieldFilter[] = ['all', 'filled', 'empty']
|
|
81
|
+
|
|
82
|
+
export const FIELD_FILTER_LABELS: Readonly<Record<FieldFilter, string>> = {
|
|
83
|
+
all: 'All',
|
|
84
|
+
filled: 'Filled',
|
|
85
|
+
empty: 'Empty',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One choice in a `FieldRow.Select`. */
|
|
89
|
+
export interface FieldOption<T extends string = string> {
|
|
90
|
+
readonly value: T
|
|
91
|
+
readonly label: string
|
|
92
|
+
/** A Lucide icon or a colour dot. Rendered before the label in both states. */
|
|
93
|
+
readonly icon?: ReactNode | undefined
|
|
94
|
+
readonly disabled?: boolean | undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One person in a `FieldRow.People`. Shaped like the thread's author (D5). */
|
|
98
|
+
export interface FieldPerson {
|
|
99
|
+
readonly id: string
|
|
100
|
+
/** Nullable by standing policy — never fabricate a display name. */
|
|
101
|
+
readonly name: string | null
|
|
102
|
+
readonly avatarUrl?: string | null | undefined
|
|
103
|
+
/** Identity colour as DATA. Omitted falls back to Avatar's hash. */
|
|
104
|
+
readonly tint?: string | null | undefined
|
|
105
|
+
/** A secondary line in the picker — an email, a role. */
|
|
106
|
+
readonly hint?: string | undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One row of the item's history (§5).
|
|
111
|
+
*
|
|
112
|
+
* The pane never derives these from the item on the client — §6 is explicit
|
|
113
|
+
* that the feed is written server-side in the same transaction as the change,
|
|
114
|
+
* "which is how the reference's activity tab stays truthful". This type is the
|
|
115
|
+
* shape that feed arrives in, and `summary` exists for the entries the
|
|
116
|
+
* standard `‹field› from ‹a› to ‹b›` sentence cannot say.
|
|
117
|
+
*/
|
|
118
|
+
export interface ActivityEntry {
|
|
119
|
+
readonly id: string
|
|
120
|
+
/** Epoch milliseconds, like the thread (D16). */
|
|
121
|
+
readonly at: number
|
|
122
|
+
readonly actor?: FieldPerson | null | undefined
|
|
123
|
+
/** User-facing field name — "Status", never `column_id` (§7 copy). */
|
|
124
|
+
readonly field?: string | undefined
|
|
125
|
+
readonly from?: string | null | undefined
|
|
126
|
+
readonly to?: string | null | undefined
|
|
127
|
+
/** Overrides the derived sentence entirely. */
|
|
128
|
+
readonly summary?: ReactNode | undefined
|
|
129
|
+
}
|