@adea-ai/ui 0.84.0 → 0.87.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/README.md +90 -1
- package/dist/NOTICE +60 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
- package/dist/components/composites/action-button/action-button.js +2 -2
- package/dist/components/composites/action-button/action-button.js.map +1 -1
- package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts +20 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.js +195 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser.d.ts +3 -0
- package/dist/components/composites/catalog-browser/catalog-browser.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser.js +378 -0
- package/dist/components/composites/catalog-browser/catalog-browser.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-detail.d.ts +21 -0
- package/dist/components/composites/catalog-browser/catalog-detail.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-detail.js +92 -0
- package/dist/components/composites/catalog-browser/catalog-detail.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-types.d.ts +78 -0
- package/dist/components/composites/catalog-browser/catalog-types.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/index.d.ts +4 -0
- package/dist/components/composites/catalog-browser/index.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/index.js +3 -0
- package/dist/components/composites/list-row/list-row.d.ts +6 -3
- package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
- package/dist/components/composites/list-row/list-row.js +69 -7
- package/dist/components/composites/list-row/list-row.js.map +1 -1
- package/dist/components/composites/settings/settings.d.ts +9 -11
- package/dist/components/composites/settings/settings.d.ts.map +1 -1
- package/dist/components/composites/settings/settings.js +48 -33
- package/dist/components/composites/settings/settings.js.map +1 -1
- package/dist/components/conversation/message-composer.js +1 -1
- package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
- package/dist/components/conversation/scroll-follow.js +95 -15
- package/dist/components/conversation/scroll-follow.js.map +1 -1
- package/dist/components/layout/app-shell/index.d.ts +1 -0
- package/dist/components/layout/app-shell/index.d.ts.map +1 -1
- package/dist/components/layout/app-shell/index.js +2 -1
- package/dist/components/layout/app-shell/skip-link.d.ts +11 -0
- package/dist/components/layout/app-shell/skip-link.d.ts.map +1 -0
- package/dist/components/layout/app-shell/skip-link.js +44 -0
- package/dist/components/layout/app-shell/skip-link.js.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts +12 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.js +283 -0
- package/dist/components/layout/floating-preview/floating-preview.js.map +1 -0
- package/dist/components/layout/floating-preview/index.d.ts +3 -0
- package/dist/components/layout/floating-preview/index.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/index.js +3 -0
- package/dist/components/layout/floating-preview/model.d.ts +74 -0
- package/dist/components/layout/floating-preview/model.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/model.js +146 -0
- package/dist/components/layout/floating-preview/model.js.map +1 -0
- package/dist/components/theme/index.d.ts +1 -0
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +2 -1
- package/dist/components/theme/theme-mode-toggle.d.ts +10 -0
- package/dist/components/theme/theme-mode-toggle.d.ts.map +1 -0
- package/dist/components/theme/theme-mode-toggle.js +82 -0
- package/dist/components/theme/theme-mode-toggle.js.map +1 -0
- package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
- package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox/checkbox.js +24 -3
- package/dist/components/ui/checkbox/checkbox.js.map +1 -1
- package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
- package/dist/components/ui/combobox/combobox.js +41 -4
- package/dist/components/ui/combobox/combobox.js.map +1 -1
- package/dist/components/ui/combobox/index.d.ts +1 -0
- package/dist/components/ui/combobox/index.d.ts.map +1 -1
- package/dist/components/ui/combobox/index.js +2 -1
- package/dist/components/ui/combobox/value-combobox.d.ts +19 -0
- package/dist/components/ui/combobox/value-combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox/value-combobox.js +42 -0
- package/dist/components/ui/combobox/value-combobox.js.map +1 -0
- package/dist/components/ui/empty/empty-state.d.ts +15 -0
- package/dist/components/ui/empty/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty/empty-state.js +96 -0
- package/dist/components/ui/empty/empty-state.js.map +1 -0
- package/dist/components/ui/empty/index.d.ts +1 -0
- package/dist/components/ui/empty/index.d.ts.map +1 -1
- package/dist/components/ui/empty/index.js +2 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts +4 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts.map +1 -1
- package/dist/components/ui/entity-icon/entity-icon.js +37 -13
- package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
- package/dist/components/ui/field/field.d.ts +36 -13
- package/dist/components/ui/field/field.d.ts.map +1 -1
- package/dist/components/ui/field/field.js +146 -24
- package/dist/components/ui/field/field.js.map +1 -1
- package/dist/components/ui/field/index.d.ts +3 -1
- package/dist/components/ui/field/index.d.ts.map +1 -1
- package/dist/components/ui/field/index.js +3 -2
- package/dist/components/ui/field/number-field.d.ts +19 -0
- package/dist/components/ui/field/number-field.d.ts.map +1 -0
- package/dist/components/ui/field/number-field.js +76 -0
- package/dist/components/ui/field/number-field.js.map +1 -0
- package/dist/components/ui/input/index.d.ts +1 -0
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/index.js +2 -1
- package/dist/components/ui/input/input-control.d.ts +11 -0
- package/dist/components/ui/input/input-control.d.ts.map +1 -0
- package/dist/components/ui/input/input-control.js +24 -0
- package/dist/components/ui/input/input-control.js.map +1 -0
- package/dist/components/ui/input/input.d.ts.map +1 -1
- package/dist/components/ui/input/input.js +56 -28
- package/dist/components/ui/input/input.js.map +1 -1
- package/dist/components/ui/input-group/index.d.ts +1 -0
- package/dist/components/ui/input-group/index.d.ts.map +1 -1
- package/dist/components/ui/input-group/index.js +2 -1
- package/dist/components/ui/input-group/input-group.d.ts.map +1 -1
- package/dist/components/ui/input-group/input-group.js +42 -5
- package/dist/components/ui/input-group/input-group.js.map +1 -1
- package/dist/components/ui/input-group/secret-input-group.d.ts +11 -0
- package/dist/components/ui/input-group/secret-input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group/secret-input-group.js +67 -0
- package/dist/components/ui/input-group/secret-input-group.js.map +1 -0
- package/dist/components/ui/native-select/native-select.d.ts.map +1 -1
- package/dist/components/ui/native-select/native-select.js +39 -4
- package/dist/components/ui/native-select/native-select.js.map +1 -1
- package/dist/components/ui/progress/progress.d.ts.map +1 -1
- package/dist/components/ui/progress/progress.js +30 -18
- package/dist/components/ui/progress/progress.js.map +1 -1
- package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group/radio-group.js +41 -4
- package/dist/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/components/ui/switch/switch.d.ts +2 -0
- package/dist/components/ui/switch/switch.d.ts.map +1 -1
- package/dist/components/ui/switch/switch.js +24 -3
- package/dist/components/ui/switch/switch.js.map +1 -1
- package/dist/components/ui/tabs/tabs.js +1 -1
- package/dist/components/ui/tabs/tabs.js.map +1 -1
- package/dist/components/ui/text-link/index.d.ts +2 -0
- package/dist/components/ui/text-link/index.d.ts.map +1 -0
- package/dist/components/ui/text-link/index.js +2 -0
- package/dist/components/ui/text-link/text-link.d.ts +9 -0
- package/dist/components/ui/text-link/text-link.d.ts.map +1 -0
- package/dist/components/ui/text-link/text-link.js +30 -0
- package/dist/components/ui/text-link/text-link.js.map +1 -0
- package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea/textarea.js +41 -4
- package/dist/components/ui/textarea/textarea.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -8
- package/dist/lib/clipboard.d.ts +3 -0
- package/dist/lib/clipboard.d.ts.map +1 -0
- package/dist/lib/clipboard.js +35 -0
- package/dist/lib/clipboard.js.map +1 -0
- package/dist/lib/download.d.ts +3 -0
- package/dist/lib/download.d.ts.map +1 -0
- package/dist/lib/download.js +31 -0
- package/dist/lib/download.js.map +1 -0
- package/dist/lib/form-field.d.ts +37 -0
- package/dist/lib/form-field.d.ts.map +1 -0
- package/dist/lib/form-field.js +47 -0
- package/dist/lib/form-field.js.map +1 -0
- package/dist/r/app-shell.json +5 -0
- package/dist/r/catalog-browser.json +47 -0
- package/dist/r/combobox.json +5 -0
- package/dist/r/empty.json +10 -1
- package/dist/r/field.json +7 -0
- package/dist/r/floating-preview.json +30 -0
- package/dist/r/input-group.json +6 -0
- package/dist/r/input.json +5 -0
- package/dist/r/lib.json +15 -0
- package/dist/r/list-row.json +1 -0
- package/dist/r/registry.json +159 -1
- package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
- package/dist/r/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-types.ts +83 -0
- package/dist/r/src/components/composites/catalog-browser/index.ts +10 -0
- package/dist/r/src/components/composites/list-row/list-row.tsx +81 -8
- package/dist/r/src/components/composites/settings/settings.tsx +67 -37
- package/dist/r/src/components/conversation/scroll-follow.ts +109 -15
- package/dist/r/src/components/layout/app-shell/index.ts +1 -0
- package/dist/r/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/dist/r/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/dist/r/src/components/layout/floating-preview/index.ts +2 -0
- package/dist/r/src/components/layout/floating-preview/model.ts +285 -0
- package/dist/r/src/components/theme/index.ts +1 -0
- package/dist/r/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/dist/r/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/dist/r/src/components/ui/combobox/combobox.tsx +24 -1
- package/dist/r/src/components/ui/combobox/index.ts +1 -0
- package/dist/r/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/dist/r/src/components/ui/empty/empty-state.tsx +71 -0
- package/dist/r/src/components/ui/empty/index.ts +1 -0
- package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/dist/r/src/components/ui/field/field.tsx +102 -5
- package/dist/r/src/components/ui/field/index.ts +5 -0
- package/dist/r/src/components/ui/field/number-field.tsx +71 -0
- package/dist/r/src/components/ui/input/index.ts +1 -0
- package/dist/r/src/components/ui/input/input-control.tsx +38 -0
- package/dist/r/src/components/ui/input/input.tsx +32 -20
- package/dist/r/src/components/ui/input-group/index.ts +1 -0
- package/dist/r/src/components/ui/input-group/input-group.tsx +25 -2
- package/dist/r/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/dist/r/src/components/ui/native-select/native-select.tsx +27 -1
- package/dist/r/src/components/ui/progress/progress.tsx +12 -8
- package/dist/r/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/dist/r/src/components/ui/switch/switch.tsx +20 -3
- package/dist/r/src/components/ui/tabs/tabs.tsx +1 -1
- package/dist/r/src/components/ui/text-link/index.ts +2 -0
- package/dist/r/src/components/ui/text-link/text-link.tsx +28 -0
- package/dist/r/src/components/ui/textarea/textarea.tsx +24 -1
- package/dist/r/src/lib/clipboard.ts +41 -0
- package/dist/r/src/lib/download.ts +29 -0
- package/dist/r/src/lib/form-field.ts +93 -0
- package/dist/r/text-link.json +22 -0
- package/dist/r/theming.json +6 -0
- package/package.json +1 -1
- package/registry.json +159 -1
- package/src/components/composites/action-button/action-button.tsx +5 -1
- package/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
- package/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
- package/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
- package/src/components/composites/catalog-browser/catalog-types.ts +83 -0
- package/src/components/composites/catalog-browser/index.ts +10 -0
- package/src/components/composites/list-row/list-row.tsx +81 -8
- package/src/components/composites/settings/settings.tsx +67 -37
- package/src/components/conversation/scroll-follow.ts +109 -15
- package/src/components/layout/app-shell/index.ts +1 -0
- package/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/src/components/layout/floating-preview/index.ts +2 -0
- package/src/components/layout/floating-preview/model.ts +285 -0
- package/src/components/theme/index.ts +1 -0
- package/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/src/components/ui/combobox/combobox.tsx +24 -1
- package/src/components/ui/combobox/index.ts +1 -0
- package/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/src/components/ui/empty/empty-state.tsx +71 -0
- package/src/components/ui/empty/index.ts +1 -0
- package/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/src/components/ui/field/field.tsx +102 -5
- package/src/components/ui/field/index.ts +5 -0
- package/src/components/ui/field/number-field.tsx +71 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/input/input-control.tsx +38 -0
- package/src/components/ui/input/input.tsx +32 -20
- package/src/components/ui/input-group/index.ts +1 -0
- package/src/components/ui/input-group/input-group.tsx +25 -2
- package/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/src/components/ui/native-select/native-select.tsx +27 -1
- package/src/components/ui/progress/progress.tsx +12 -8
- package/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/src/components/ui/switch/switch.tsx +20 -3
- package/src/components/ui/tabs/tabs.tsx +1 -1
- package/src/components/ui/text-link/index.ts +2 -0
- package/src/components/ui/text-link/text-link.tsx +28 -0
- package/src/components/ui/textarea/textarea.tsx +24 -1
- package/src/index.ts +4 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/download.ts +29 -0
- package/src/lib/form-field.ts +93 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Adea contributors
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
* Geometry translated from t3code apps/web/src/components/preview/
|
|
5
|
+
* previewMiniPlayerLayout.ts, issue #422, revision
|
|
6
|
+
* 77bca8b2d76a1f42552e5eee7d277fcb1160347a (MIT). Runtime and device
|
|
7
|
+
* behavior are excluded. See NOTICE.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12
|
|
11
|
+
export const PREVIEW_MINI_PLAYER_CORNER_RADIUS = 12
|
|
12
|
+
export interface PreviewMiniPlayerSize {
|
|
13
|
+
readonly width: number
|
|
14
|
+
readonly height: number
|
|
15
|
+
}
|
|
16
|
+
export interface PreviewMiniPlayerPosition {
|
|
17
|
+
readonly x: number
|
|
18
|
+
readonly y: number
|
|
19
|
+
}
|
|
20
|
+
export interface PreviewMiniPlayerFrame extends PreviewMiniPlayerPosition, PreviewMiniPlayerSize {}
|
|
21
|
+
|
|
22
|
+
export type BrowserViewportResizeDirection =
|
|
23
|
+
| 'north'
|
|
24
|
+
| 'south'
|
|
25
|
+
| 'east'
|
|
26
|
+
| 'west'
|
|
27
|
+
| 'northwest'
|
|
28
|
+
| 'northeast'
|
|
29
|
+
| 'southwest'
|
|
30
|
+
| 'southeast'
|
|
31
|
+
|
|
32
|
+
const PREVIEW_MINI_PLAYER_DEFAULT_BOX = { width: 320, height: 320 } as const
|
|
33
|
+
const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const
|
|
34
|
+
|
|
35
|
+
interface HorizontalSpan {
|
|
36
|
+
readonly left: number
|
|
37
|
+
readonly right: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The composer stack docked to the bottom edge, in container coordinates. It
|
|
42
|
+
* only reserves the columns it covers, so the margins beside it stay open all
|
|
43
|
+
* the way down.
|
|
44
|
+
*/
|
|
45
|
+
export interface PreviewMiniPlayerObstacles {
|
|
46
|
+
readonly composer: (HorizontalSpan & { readonly height: number }) | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const NO_PREVIEW_MINI_PLAYER_OBSTACLES: PreviewMiniPlayerObstacles = { composer: null }
|
|
50
|
+
|
|
51
|
+
const spanOf = (x: number, width: number): HorizontalSpan => ({ left: x, right: x + width })
|
|
52
|
+
|
|
53
|
+
const spansOverlap = (a: HorizontalSpan, b: HorizontalSpan) => a.left < b.right && a.right > b.left
|
|
54
|
+
|
|
55
|
+
/** The lowest row (before the edge gap) open to a player covering these columns. */
|
|
56
|
+
function floorFor(
|
|
57
|
+
span: HorizontalSpan,
|
|
58
|
+
container: PreviewMiniPlayerSize,
|
|
59
|
+
obstacles: PreviewMiniPlayerObstacles
|
|
60
|
+
): number {
|
|
61
|
+
const { composer } = obstacles
|
|
62
|
+
return composer && spansOverlap(span, composer)
|
|
63
|
+
? container.height - Math.max(0, composer.height)
|
|
64
|
+
: container.height
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The box a stored size is fitted into. A player with a position keeps the
|
|
69
|
+
* rows its own columns have, so a tall frame parked beside the composer
|
|
70
|
+
* survives the next layout pass; without one it takes the rows above the
|
|
71
|
+
* composer, which every column has.
|
|
72
|
+
*/
|
|
73
|
+
const availableArea = (
|
|
74
|
+
container: PreviewMiniPlayerSize,
|
|
75
|
+
obstacles: PreviewMiniPlayerObstacles,
|
|
76
|
+
span: HorizontalSpan | null
|
|
77
|
+
): PreviewMiniPlayerSize => ({
|
|
78
|
+
width: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
|
|
79
|
+
height:
|
|
80
|
+
(span
|
|
81
|
+
? floorFor(span, container, obstacles)
|
|
82
|
+
: container.height - Math.max(0, obstacles.composer?.height ?? 0)) -
|
|
83
|
+
PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Width is the player's only free dimension; height always follows the source
|
|
88
|
+
* aspect ratio so the content fills the box without letterboxing. The player
|
|
89
|
+
* never grows past the source's own size, and a tight container wins over the
|
|
90
|
+
* minimum.
|
|
91
|
+
*/
|
|
92
|
+
function fitPreviewMiniPlayerWidth(
|
|
93
|
+
desiredWidth: number,
|
|
94
|
+
source: PreviewMiniPlayerSize,
|
|
95
|
+
max: PreviewMiniPlayerSize
|
|
96
|
+
): PreviewMiniPlayerSize {
|
|
97
|
+
const aspectRatio = source.width / source.height
|
|
98
|
+
const width = Math.min(
|
|
99
|
+
Math.max(
|
|
100
|
+
desiredWidth,
|
|
101
|
+
PREVIEW_MINI_PLAYER_MIN_SIZE.width,
|
|
102
|
+
PREVIEW_MINI_PLAYER_MIN_SIZE.height * aspectRatio
|
|
103
|
+
),
|
|
104
|
+
source.width,
|
|
105
|
+
Math.max(1, max.width),
|
|
106
|
+
Math.max(1, max.height * aspectRatio)
|
|
107
|
+
)
|
|
108
|
+
return { width: Math.round(width), height: Math.round(width / aspectRatio) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultPreviewMiniPlayerWidth(source: PreviewMiniPlayerSize): number {
|
|
112
|
+
return Math.min(
|
|
113
|
+
PREVIEW_MINI_PLAYER_DEFAULT_BOX.width,
|
|
114
|
+
(PREVIEW_MINI_PLAYER_DEFAULT_BOX.height * source.width) / source.height
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const clampToContainer = (
|
|
119
|
+
position: PreviewMiniPlayerPosition,
|
|
120
|
+
container: PreviewMiniPlayerSize,
|
|
121
|
+
player: PreviewMiniPlayerSize,
|
|
122
|
+
bottom = container.height
|
|
123
|
+
): PreviewMiniPlayerPosition => ({
|
|
124
|
+
x: Math.min(
|
|
125
|
+
Math.max(position.x, PREVIEW_MINI_PLAYER_EDGE_GAP),
|
|
126
|
+
Math.max(
|
|
127
|
+
PREVIEW_MINI_PLAYER_EDGE_GAP,
|
|
128
|
+
container.width - player.width - PREVIEW_MINI_PLAYER_EDGE_GAP
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
y: Math.min(
|
|
132
|
+
Math.max(position.y, PREVIEW_MINI_PLAYER_EDGE_GAP),
|
|
133
|
+
Math.max(PREVIEW_MINI_PLAYER_EDGE_GAP, bottom - player.height - PREVIEW_MINI_PLAYER_EDGE_GAP)
|
|
134
|
+
),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const overlapsObstacle = (
|
|
138
|
+
position: PreviewMiniPlayerPosition,
|
|
139
|
+
player: PreviewMiniPlayerSize,
|
|
140
|
+
container: PreviewMiniPlayerSize,
|
|
141
|
+
obstacles: PreviewMiniPlayerObstacles
|
|
142
|
+
): boolean =>
|
|
143
|
+
position.y + player.height > floorFor(spanOf(position.x, player.width), container, obstacles)
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Keeps the player inside the container and off the composer. An overlapping
|
|
147
|
+
* player is pushed out along whichever side needs the smaller move, so a drag
|
|
148
|
+
* slides along the composer into the margin beside it instead of stopping at
|
|
149
|
+
* its top edge; when no side leaves it fully clear it sits above the composer.
|
|
150
|
+
*/
|
|
151
|
+
export function clampPreviewMiniPlayerPosition(
|
|
152
|
+
position: PreviewMiniPlayerPosition,
|
|
153
|
+
container: PreviewMiniPlayerSize,
|
|
154
|
+
player: PreviewMiniPlayerSize,
|
|
155
|
+
obstacles: PreviewMiniPlayerObstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES
|
|
156
|
+
): PreviewMiniPlayerPosition {
|
|
157
|
+
const inside = clampToContainer(position, container, player)
|
|
158
|
+
const { composer } = obstacles
|
|
159
|
+
if (!composer || !overlapsObstacle(inside, player, container, obstacles)) return inside
|
|
160
|
+
const gap = PREVIEW_MINI_PLAYER_EDGE_GAP
|
|
161
|
+
const above = { x: inside.x, y: container.height - composer.height - gap - player.height }
|
|
162
|
+
const beside = [
|
|
163
|
+
{ x: composer.left - gap - player.width, y: inside.y },
|
|
164
|
+
{ x: composer.right + gap, y: inside.y },
|
|
165
|
+
]
|
|
166
|
+
let best = clampToContainer(above, container, player)
|
|
167
|
+
let bestDistance = Math.abs(best.y - inside.y)
|
|
168
|
+
for (const candidate of beside) {
|
|
169
|
+
const clamped = clampToContainer(candidate, container, player)
|
|
170
|
+
if (clamped.x !== candidate.x || overlapsObstacle(candidate, player, container, obstacles)) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
const distance = Math.abs(candidate.x - inside.x)
|
|
174
|
+
if (distance < bestDistance) {
|
|
175
|
+
best = candidate
|
|
176
|
+
bestDistance = distance
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return best
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolves the on-screen frame from the stored width and position. Clamping
|
|
184
|
+
* happens here on every layout pass instead of being written back to the
|
|
185
|
+
* store, so a temporarily narrow container never destroys the user's chosen
|
|
186
|
+
* width. A player without a position sits in the top-right corner.
|
|
187
|
+
*/
|
|
188
|
+
export function resolvePreviewMiniPlayerFrame(input: {
|
|
189
|
+
readonly width: number | null
|
|
190
|
+
readonly position: PreviewMiniPlayerPosition | null
|
|
191
|
+
readonly source: PreviewMiniPlayerSize
|
|
192
|
+
readonly container: PreviewMiniPlayerSize
|
|
193
|
+
readonly obstacles?: PreviewMiniPlayerObstacles
|
|
194
|
+
}): PreviewMiniPlayerFrame {
|
|
195
|
+
const { width, position, source, container, obstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES } = input
|
|
196
|
+
const size = fitPreviewMiniPlayerWidth(
|
|
197
|
+
width ?? defaultPreviewMiniPlayerWidth(source),
|
|
198
|
+
source,
|
|
199
|
+
availableArea(container, obstacles, position && width ? spanOf(position.x, width) : null)
|
|
200
|
+
)
|
|
201
|
+
const anchored = position ?? {
|
|
202
|
+
x: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - size.width,
|
|
203
|
+
y: PREVIEW_MINI_PLAYER_EDGE_GAP,
|
|
204
|
+
}
|
|
205
|
+
return { ...clampPreviewMiniPlayerPosition(anchored, container, size, obstacles), ...size }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resizes from any edge or corner while holding the aspect ratio. The edge
|
|
210
|
+
* opposite the dragged one stays anchored, so growth stops at the container
|
|
211
|
+
* on that axis and the pointer keeps tracking the grabbed edge.
|
|
212
|
+
*/
|
|
213
|
+
export function resizePreviewMiniPlayer(input: {
|
|
214
|
+
readonly start: PreviewMiniPlayerFrame
|
|
215
|
+
readonly direction: BrowserViewportResizeDirection
|
|
216
|
+
readonly delta: PreviewMiniPlayerPosition
|
|
217
|
+
readonly source: PreviewMiniPlayerSize
|
|
218
|
+
readonly container: PreviewMiniPlayerSize
|
|
219
|
+
readonly obstacles?: PreviewMiniPlayerObstacles
|
|
220
|
+
}): PreviewMiniPlayerFrame {
|
|
221
|
+
const {
|
|
222
|
+
start,
|
|
223
|
+
direction,
|
|
224
|
+
delta,
|
|
225
|
+
source,
|
|
226
|
+
container,
|
|
227
|
+
obstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES,
|
|
228
|
+
} = input
|
|
229
|
+
const east = direction.includes('east')
|
|
230
|
+
const west = direction.includes('west')
|
|
231
|
+
const north = direction.includes('north')
|
|
232
|
+
const south = direction.includes('south')
|
|
233
|
+
const right = start.x + start.width
|
|
234
|
+
const bottom = start.y + start.height
|
|
235
|
+
const floor = floorFor(spanOf(start.x, start.width), container, obstacles)
|
|
236
|
+
const max = {
|
|
237
|
+
width: west
|
|
238
|
+
? right - PREVIEW_MINI_PLAYER_EDGE_GAP
|
|
239
|
+
: east
|
|
240
|
+
? container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - start.x
|
|
241
|
+
: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
|
|
242
|
+
height: north
|
|
243
|
+
? bottom - PREVIEW_MINI_PLAYER_EDGE_GAP
|
|
244
|
+
: south
|
|
245
|
+
? floor - PREVIEW_MINI_PLAYER_EDGE_GAP - start.y
|
|
246
|
+
: floor - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
|
|
247
|
+
}
|
|
248
|
+
const desiredWidth = start.width + (east ? delta.x : west ? -delta.x : 0)
|
|
249
|
+
const desiredHeight = start.height + (south ? delta.y : north ? -delta.y : 0)
|
|
250
|
+
const horizontal = east || west
|
|
251
|
+
const vertical = north || south
|
|
252
|
+
const widthLeads =
|
|
253
|
+
horizontal && !vertical
|
|
254
|
+
? true
|
|
255
|
+
: vertical && !horizontal
|
|
256
|
+
? false
|
|
257
|
+
: Math.abs(desiredWidth - start.width) / start.width >=
|
|
258
|
+
Math.abs(desiredHeight - start.height) / start.height
|
|
259
|
+
const size = fitPreviewMiniPlayerWidth(
|
|
260
|
+
widthLeads ? desiredWidth : (desiredHeight * source.width) / source.height,
|
|
261
|
+
source,
|
|
262
|
+
max
|
|
263
|
+
)
|
|
264
|
+
const position = clampPreviewMiniPlayerPosition(
|
|
265
|
+
{ x: west ? right - size.width : start.x, y: north ? bottom - size.height : start.y },
|
|
266
|
+
container,
|
|
267
|
+
size,
|
|
268
|
+
obstacles
|
|
269
|
+
)
|
|
270
|
+
return { ...position, ...size }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Product-neutral names for new consumers. The PreviewMiniPlayer names stay
|
|
274
|
+
// exported for Adea Dev Runtime's existing browser model API.
|
|
275
|
+
export type FloatingPreviewSize = PreviewMiniPlayerSize
|
|
276
|
+
export type FloatingPreviewPosition = PreviewMiniPlayerPosition
|
|
277
|
+
export type FloatingPreviewFrame = PreviewMiniPlayerFrame
|
|
278
|
+
export type FloatingPreviewResizeDirection = BrowserViewportResizeDirection
|
|
279
|
+
export type FloatingPreviewObstacles = PreviewMiniPlayerObstacles
|
|
280
|
+
export const FLOATING_PREVIEW_EDGE_GAP = PREVIEW_MINI_PLAYER_EDGE_GAP
|
|
281
|
+
export const FLOATING_PREVIEW_CORNER_RADIUS = PREVIEW_MINI_PLAYER_CORNER_RADIUS
|
|
282
|
+
export const NO_FLOATING_PREVIEW_OBSTACLES = NO_PREVIEW_MINI_PLAYER_OBSTACLES
|
|
283
|
+
export const resolveFloatingPreviewFrame = resolvePreviewMiniPlayerFrame
|
|
284
|
+
export const resizeFloatingPreview = resizePreviewMiniPlayer
|
|
285
|
+
export const clampFloatingPreviewPosition = clampPreviewMiniPlayerPosition
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Monitor, Moon, Sun } from 'lucide-solid'
|
|
2
|
+
import type { ComponentProps } from 'solid-js'
|
|
3
|
+
import { For, splitProps } from 'solid-js'
|
|
4
|
+
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group/toggle-group'
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
|
6
|
+
|
|
7
|
+
const APPEARANCES = [
|
|
8
|
+
{ value: 'light', label: 'Light', icon: Sun },
|
|
9
|
+
{ value: 'dark', label: 'Dark', icon: Moon },
|
|
10
|
+
{ value: 'system', label: 'System', icon: Monitor },
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
/** Controlled appearance selection for hosts that own their preference storage. */
|
|
14
|
+
export type ThemeModeToggleProps = Omit<
|
|
15
|
+
ComponentProps<typeof ToggleGroup>,
|
|
16
|
+
'children' | 'value' | 'onChange' | 'multiple'
|
|
17
|
+
> & {
|
|
18
|
+
mode: 'light' | 'dark' | 'system'
|
|
19
|
+
onModeChange: (mode: 'light' | 'dark' | 'system') => void
|
|
20
|
+
withLabels?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ThemeModeToggle(props: ThemeModeToggleProps) {
|
|
24
|
+
const [local, rest] = splitProps(props, ['mode', 'onModeChange', 'withLabels'])
|
|
25
|
+
return (
|
|
26
|
+
<ToggleGroup
|
|
27
|
+
{...rest}
|
|
28
|
+
attached
|
|
29
|
+
multiple={false}
|
|
30
|
+
value={local.mode}
|
|
31
|
+
onChange={(value: string | null) => {
|
|
32
|
+
const option = APPEARANCES.find((candidate) => candidate.value === value)
|
|
33
|
+
if (option) local.onModeChange(option.value)
|
|
34
|
+
}}
|
|
35
|
+
aria-label={rest['aria-label'] ?? 'Appearance'}
|
|
36
|
+
>
|
|
37
|
+
<For each={APPEARANCES}>
|
|
38
|
+
{(appearance) => (
|
|
39
|
+
<Tooltip>
|
|
40
|
+
<TooltipTrigger
|
|
41
|
+
as={ToggleGroupItem}
|
|
42
|
+
value={appearance.value}
|
|
43
|
+
variant="outline"
|
|
44
|
+
size={local.withLabels ? 'sm' : 'icon-sm'}
|
|
45
|
+
aria-label={appearance.label}
|
|
46
|
+
>
|
|
47
|
+
<appearance.icon aria-hidden="true" />
|
|
48
|
+
{local.withLabels ? <span>{appearance.label}</span> : null}
|
|
49
|
+
</TooltipTrigger>
|
|
50
|
+
<TooltipContent>Use {appearance.label.toLowerCase()} appearance</TooltipContent>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
)}
|
|
53
|
+
</For>
|
|
54
|
+
</ToggleGroup>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Checkbox as KobalteCheckbox } from '@kobalte/core/checkbox'
|
|
|
2
2
|
import { Check, Minus } from 'lucide-solid'
|
|
3
3
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
4
4
|
import { splitProps } from 'solid-js'
|
|
5
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
5
6
|
import { cn } from '../../../lib/utils'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -18,6 +19,8 @@ import { cn } from '../../../lib/utils'
|
|
|
18
19
|
* the ring lives on the control and reads the input's focus state.
|
|
19
20
|
*/
|
|
20
21
|
export type CheckboxProps = Omit<ComponentProps<typeof KobalteCheckbox>, 'children'> & {
|
|
22
|
+
/** ID for the hidden native checkbox input; useful with an external label. */
|
|
23
|
+
inputId?: string
|
|
21
24
|
controlClass?: string
|
|
22
25
|
labelClass?: string
|
|
23
26
|
descriptionClass?: string
|
|
@@ -44,12 +47,24 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
44
47
|
'aria-labelledby',
|
|
45
48
|
'aria-describedby',
|
|
46
49
|
'aria-invalid',
|
|
50
|
+
'aria-errormessage',
|
|
47
51
|
'aria-busy',
|
|
52
|
+
'id',
|
|
53
|
+
'inputId',
|
|
48
54
|
'title',
|
|
49
55
|
])
|
|
56
|
+
const field = useFormFieldControl('control', {
|
|
57
|
+
id: local.inputId ?? local.id,
|
|
58
|
+
'aria-label': local['aria-label'],
|
|
59
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
60
|
+
'aria-describedby': local['aria-describedby'],
|
|
61
|
+
'aria-invalid': local['aria-invalid'],
|
|
62
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
63
|
+
})
|
|
50
64
|
|
|
51
65
|
return (
|
|
52
66
|
<KobalteCheckbox
|
|
67
|
+
id={field ? undefined : local.id}
|
|
53
68
|
title={local.title}
|
|
54
69
|
class={cn('group/checkbox flex items-start gap-2.5', local.class)}
|
|
55
70
|
{...rest}
|
|
@@ -60,10 +75,12 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
60
75
|
* box would announce its state with no subject.
|
|
61
76
|
*/}
|
|
62
77
|
<KobalteCheckbox.Input
|
|
78
|
+
id={field?.id ?? local.inputId}
|
|
63
79
|
aria-label={local['aria-label']}
|
|
64
|
-
aria-labelledby={local['aria-labelledby']}
|
|
65
|
-
aria-describedby={local['aria-describedby']}
|
|
66
|
-
aria-invalid={local['aria-invalid']}
|
|
80
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
81
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
82
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
83
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
67
84
|
aria-busy={local['aria-busy']}
|
|
68
85
|
title={local.title}
|
|
69
86
|
/>
|
|
@@ -3,6 +3,7 @@ import { Check, ChevronDown, X } from 'lucide-solid'
|
|
|
3
3
|
import type { ComponentProps } from 'solid-js'
|
|
4
4
|
import { Show, splitProps } from 'solid-js'
|
|
5
5
|
import { menuContentPadding, menuItem, popoverArrow } from '../../../lib/overlay'
|
|
6
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
6
7
|
import { cn } from '../../../lib/utils'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -43,10 +44,32 @@ export function ComboboxControl(props: ComponentProps<typeof KobalteCombobox.Con
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function ComboboxInput(props: ComponentProps<typeof KobalteCombobox.Input>) {
|
|
46
|
-
const [local, rest] = splitProps(props, [
|
|
47
|
+
const [local, rest] = splitProps(props, [
|
|
48
|
+
'class',
|
|
49
|
+
'id',
|
|
50
|
+
'aria-label',
|
|
51
|
+
'aria-labelledby',
|
|
52
|
+
'aria-describedby',
|
|
53
|
+
'aria-invalid',
|
|
54
|
+
'aria-errormessage',
|
|
55
|
+
])
|
|
56
|
+
const field = useFormFieldControl('control', {
|
|
57
|
+
id: local.id,
|
|
58
|
+
'aria-label': local['aria-label'],
|
|
59
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
60
|
+
'aria-describedby': local['aria-describedby'],
|
|
61
|
+
'aria-invalid': local['aria-invalid'],
|
|
62
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
63
|
+
})
|
|
47
64
|
|
|
48
65
|
return (
|
|
49
66
|
<KobalteCombobox.Input
|
|
67
|
+
id={field?.id ?? local.id}
|
|
68
|
+
aria-label={local['aria-label']}
|
|
69
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
70
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
71
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
72
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
50
73
|
class={cn(
|
|
51
74
|
'h-full flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground',
|
|
52
75
|
'disabled:cursor-not-allowed',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { splitProps, type ComponentProps, type JSX } from 'solid-js'
|
|
2
|
+
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from './combobox'
|
|
3
|
+
|
|
4
|
+
export type ValueComboboxChoice =
|
|
5
|
+
| { value: string; label: string | number; textValue?: string }
|
|
6
|
+
| { value: string; label: JSX.Element; textValue: string }
|
|
7
|
+
|
|
8
|
+
export type ValueComboboxProps = Omit<ComponentProps<typeof ComboboxInput>, 'value'> & {
|
|
9
|
+
value: string
|
|
10
|
+
choices: ValueComboboxChoice[]
|
|
11
|
+
onValueChange: (value: string) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A single-choice combobox for a controlled string value and labelled choices. */
|
|
15
|
+
export function ValueCombobox(props: ValueComboboxProps) {
|
|
16
|
+
const [local, rest] = splitProps(props, ['value', 'choices', 'onValueChange'])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Combobox
|
|
20
|
+
options={local.choices}
|
|
21
|
+
optionValue="value"
|
|
22
|
+
optionTextValue={(choice) => choice.textValue ?? textFromLabel(choice.label)}
|
|
23
|
+
optionLabel={(choice) => choice.textValue ?? textFromLabel(choice.label)}
|
|
24
|
+
value={local.choices.find((choice) => String(choice.value) === local.value) ?? null}
|
|
25
|
+
onChange={(option) => option && local.onValueChange(String(option.value))}
|
|
26
|
+
itemComponent={(itemProps) => (
|
|
27
|
+
<ComboboxItem item={itemProps.item}>{itemProps.item.rawValue.label}</ComboboxItem>
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<ComboboxInput {...rest} />
|
|
31
|
+
<ComboboxContent />
|
|
32
|
+
</Combobox>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function textFromLabel(label: JSX.Element) {
|
|
37
|
+
return typeof label === 'string' || typeof label === 'number' ? String(label) : ''
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Search } from 'lucide-solid'
|
|
2
|
+
import type { ComponentProps, JSX } from 'solid-js'
|
|
3
|
+
import { Show, splitProps } from 'solid-js'
|
|
4
|
+
import { ActionButton } from '../../composites/action-button'
|
|
5
|
+
import { Spinner } from '../spinner'
|
|
6
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './empty'
|
|
7
|
+
|
|
8
|
+
export type EmptyStateProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'aria-live'> & {
|
|
9
|
+
title: string
|
|
10
|
+
detail: string
|
|
11
|
+
busy?: boolean
|
|
12
|
+
icon?: JSX.Element
|
|
13
|
+
announceAs?: 'alert' | 'status'
|
|
14
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
|
|
15
|
+
action?: () => void
|
|
16
|
+
actionLabel?: string
|
|
17
|
+
actionTooltip?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A semantic empty, loading, or error state built from the shared empty parts. */
|
|
21
|
+
export function EmptyState(props: EmptyStateProps) {
|
|
22
|
+
const [local, rest] = splitProps(props, [
|
|
23
|
+
'title',
|
|
24
|
+
'detail',
|
|
25
|
+
'busy',
|
|
26
|
+
'icon',
|
|
27
|
+
'announceAs',
|
|
28
|
+
'headingLevel',
|
|
29
|
+
'action',
|
|
30
|
+
'actionLabel',
|
|
31
|
+
'actionTooltip',
|
|
32
|
+
'class',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Empty
|
|
37
|
+
{...rest}
|
|
38
|
+
class={local.class}
|
|
39
|
+
role={local.announceAs}
|
|
40
|
+
aria-live={local.announceAs === 'status' ? 'polite' : undefined}
|
|
41
|
+
aria-busy={local.busy || undefined}
|
|
42
|
+
>
|
|
43
|
+
<EmptyHeader>
|
|
44
|
+
<Show when={local.busy || local.icon}>
|
|
45
|
+
<EmptyMedia variant="icon">
|
|
46
|
+
{local.busy ? (
|
|
47
|
+
<Spinner label={false} aria-hidden="true" />
|
|
48
|
+
) : (
|
|
49
|
+
(local.icon ?? <Search aria-hidden="true" />)
|
|
50
|
+
)}
|
|
51
|
+
</EmptyMedia>
|
|
52
|
+
</Show>
|
|
53
|
+
<EmptyTitle role="heading" aria-level={local.headingLevel ?? 1}>
|
|
54
|
+
{local.title}
|
|
55
|
+
</EmptyTitle>
|
|
56
|
+
<EmptyDescription>{local.detail}</EmptyDescription>
|
|
57
|
+
</EmptyHeader>
|
|
58
|
+
<Show when={local.action}>
|
|
59
|
+
<EmptyContent>
|
|
60
|
+
<ActionButton
|
|
61
|
+
tooltip={local.actionTooltip ?? local.actionLabel ?? 'Try again'}
|
|
62
|
+
aria-label={local.actionLabel ?? 'Try again'}
|
|
63
|
+
onClick={local.action}
|
|
64
|
+
>
|
|
65
|
+
{local.actionLabel ?? 'Try again'}
|
|
66
|
+
</ActionButton>
|
|
67
|
+
</EmptyContent>
|
|
68
|
+
</Show>
|
|
69
|
+
</Empty>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
Show,
|
|
4
|
+
splitProps,
|
|
5
|
+
type Component,
|
|
6
|
+
type ComponentProps,
|
|
7
|
+
type JSX,
|
|
8
|
+
} from 'solid-js'
|
|
2
9
|
import { Dynamic } from 'solid-js/web'
|
|
3
10
|
import { cva, type VariantProps } from '../../../lib/variants'
|
|
4
11
|
import { cn } from '../../../lib/utils'
|
|
@@ -16,7 +23,8 @@ import { cn } from '../../../lib/utils'
|
|
|
16
23
|
*
|
|
17
24
|
* 1. **A glyph is always present.** A brand with no logo gets a monogram, not an
|
|
18
25
|
* empty box. `fallback` takes the name and returns the glyph, so a caller can
|
|
19
|
-
* choose initials, a domain letter, or a kind icon.
|
|
26
|
+
* choose initials, a domain letter, or a kind icon. When `src` is supplied,
|
|
27
|
+
* its contained image overlays that glyph and failure reveals the glyph below.
|
|
20
28
|
* 2. **The tile owns its own contrast.** `tone` tints the background while
|
|
21
29
|
* monogram text keeps the body foreground; vector glyphs retain the status
|
|
22
30
|
* colour. A caller never picks a colour pair or ships a glyph that disappears.
|
|
@@ -67,6 +75,8 @@ export type EntityIconProps = Omit<ComponentProps<'span'>, 'children'> &
|
|
|
67
75
|
EntityIconVariantProps & {
|
|
68
76
|
/** The entity's name. Used for the accessible label and, via `fallback`, the monogram. */
|
|
69
77
|
name: string
|
|
78
|
+
/** Optional image URL. The image is decorative; `name` remains the accessible label. */
|
|
79
|
+
src?: string
|
|
70
80
|
/**
|
|
71
81
|
* A logo or icon — a Lucide icon reference (`icon={Wrench}`) or an element
|
|
72
82
|
* (`icon={<Wrench />}`). Both are accepted because both are what a caller
|
|
@@ -108,9 +118,27 @@ function Glyph(props: { glyph: Glyph }) {
|
|
|
108
118
|
)
|
|
109
119
|
}
|
|
110
120
|
|
|
121
|
+
function EntityIconImage(props: { src: string }) {
|
|
122
|
+
const [failed, setFailed] = createSignal(false)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Show when={!failed()}>
|
|
126
|
+
<img
|
|
127
|
+
data-slot="entity-icon-image"
|
|
128
|
+
src={props.src}
|
|
129
|
+
alt=""
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
class="pointer-events-none absolute inset-0 size-full object-contain"
|
|
132
|
+
onError={() => setFailed(true)}
|
|
133
|
+
/>
|
|
134
|
+
</Show>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
export function EntityIcon(props: EntityIconProps) {
|
|
112
139
|
const [local, rest] = splitProps(props, [
|
|
113
140
|
'name',
|
|
141
|
+
'src',
|
|
114
142
|
'icon',
|
|
115
143
|
'fallback',
|
|
116
144
|
'badge',
|
|
@@ -146,6 +174,9 @@ export function EntityIcon(props: EntityIconProps) {
|
|
|
146
174
|
>
|
|
147
175
|
{(value) => <Glyph glyph={value()} />}
|
|
148
176
|
</Show>
|
|
177
|
+
<Show when={local.src} keyed>
|
|
178
|
+
{(src) => <EntityIconImage src={src} />}
|
|
179
|
+
</Show>
|
|
149
180
|
<Show when={local.badge}>
|
|
150
181
|
<span class="absolute right-0 bottom-0 translate-x-1/4 translate-y-1/4">{local.badge}</span>
|
|
151
182
|
</Show>
|