bref-ui-themes 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +110 -2
- package/dist/components/button/button.svelte +145 -0
- package/dist/components/button/button.svelte.d.ts +4 -0
- package/dist/components/checkbox/checkbox.svelte +156 -0
- package/dist/components/checkbox/checkbox.svelte.d.ts +4 -0
- package/dist/components/checkbox/types.d.ts +6 -0
- package/dist/components/checkbox/types.js +1 -0
- package/dist/components/dialog/dialog.svelte +184 -0
- package/dist/components/dialog/dialog.svelte.d.ts +4 -0
- package/dist/components/dialog/footer.svelte +26 -0
- package/dist/components/dialog/footer.svelte.d.ts +7 -0
- package/dist/components/dialog/header.svelte +66 -0
- package/dist/components/dialog/header.svelte.d.ts +4 -0
- package/dist/components/icon/icon.svelte +110 -0
- package/dist/components/icon/icon.svelte.d.ts +4 -0
- package/dist/components/index.d.ts +14 -0
- package/dist/components/index.js +14 -0
- package/dist/components/pill/content.svelte +30 -0
- package/dist/components/pill/content.svelte.d.ts +4 -0
- package/dist/components/pill/on-delete.svelte +55 -0
- package/dist/components/pill/on-delete.svelte.d.ts +4 -0
- package/dist/components/pill/pill.svelte +179 -0
- package/dist/components/pill/pill.svelte.d.ts +4 -0
- package/dist/components/pill-choice-group/pill-choice-group.svelte +150 -0
- package/dist/components/pill-choice-group/pill-choice-group.svelte.d.ts +4 -0
- package/dist/components/pill-group/container.svelte +40 -0
- package/dist/components/pill-group/container.svelte.d.ts +4 -0
- package/dist/components/pill-group/pill-group.svelte +29 -0
- package/dist/components/pill-group/pill-group.svelte.d.ts +4 -0
- package/dist/components/pill-group/types.d.ts +14 -0
- package/dist/components/pill-group/types.js +1 -0
- package/dist/components/popover/popover.svelte +111 -0
- package/dist/components/popover/popover.svelte.d.ts +4 -0
- package/dist/components/progress/progress.svelte +242 -0
- package/dist/components/progress/progress.svelte.d.ts +4 -0
- package/dist/components/select/empty.svelte +24 -0
- package/dist/components/select/empty.svelte.d.ts +4 -0
- package/dist/components/select/list.svelte +28 -0
- package/dist/components/select/list.svelte.d.ts +4 -0
- package/dist/components/select/option.svelte +78 -0
- package/dist/components/select/option.svelte.d.ts +4 -0
- package/dist/components/select/select.svelte +73 -0
- package/dist/components/select/select.svelte.d.ts +4 -0
- package/dist/components/select/trigger.svelte +109 -0
- package/dist/components/select/trigger.svelte.d.ts +4 -0
- package/dist/components/spinner/spinner.svelte +101 -0
- package/dist/components/spinner/spinner.svelte.d.ts +4 -0
- package/dist/components/surface/surface.svelte +101 -0
- package/dist/components/surface/surface.svelte.d.ts +4 -0
- package/dist/components/tabs/item.svelte +125 -0
- package/dist/components/tabs/item.svelte.d.ts +4 -0
- package/dist/components/tabs/tabs.svelte +124 -0
- package/dist/components/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/text-input/text-input.svelte +149 -0
- package/dist/components/text-input/text-input.svelte.d.ts +4 -0
- package/dist/components/theme/surface.d.ts +3 -0
- package/dist/components/theme/surface.js +11 -0
- package/dist/elsewhere/index.d.ts +2 -0
- package/dist/elsewhere/index.js +2 -0
- package/dist/elsewhere/theme.svelte +45 -0
- package/dist/elsewhere/theme.svelte.d.ts +9 -0
- package/dist/forma/index.d.ts +2 -0
- package/dist/forma/index.js +2 -0
- package/dist/forma/theme.svelte +44 -0
- package/dist/forma/theme.svelte.d.ts +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/relay/index.d.ts +2 -0
- package/dist/relay/index.js +2 -0
- package/dist/relay/theme.svelte +47 -0
- package/dist/relay/theme.svelte.d.ts +8 -0
- package/dist/shared/base.svelte +91 -0
- package/dist/shared/base.svelte.d.ts +26 -0
- package/package.json +85 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bref contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bref-ui-themes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reusable Svelte 5 themes for [bref-ui](https://github.com/feuersteiner/bref-ui), with interactive demos and native CSS. Version 0.1.0 is an unpublished review candidate built on SvelteKit 3 and published Bref 2.2.0.
|
|
4
|
+
|
|
5
|
+
[Explore the showcase](https://feuersteiner.github.io/bref-ui-themes/) · [Bref documentation](https://feuersteiner.github.io/bref-ui/) · [Join Discord](https://discord.gg/JpgQRTUsVS)
|
|
6
|
+
|
|
7
|
+
## Local development
|
|
8
|
+
|
|
9
|
+
Requires Bun 1.3.4 or newer. The scaffold uses Svelte 5, SvelteKit 3, Vite 8 and `svelte-package` 3.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun install
|
|
13
|
+
bun run dev # http://localhost:5173
|
|
14
|
+
bun run check
|
|
15
|
+
bun run lint
|
|
16
|
+
bun run package # library → dist; includes publint
|
|
17
|
+
bun run build:site # static showcase → build
|
|
18
|
+
bun run build # site + library
|
|
19
|
+
bun run preview # preview the production site
|
|
20
|
+
bun pm pack # creates a local npm tarball; does not publish
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The restored GitHub Pages workflow builds `build:site` on pushes to `main`, using the Pages base path and uploading `build`. To reproduce the project-path build locally:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
BASE_PATH=/bref-ui-themes bun run build:site
|
|
27
|
+
BASE_PATH=/bref-ui-themes bun run preview # http://localhost:4173/bref-ui-themes/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Showcase and demo sites
|
|
31
|
+
|
|
32
|
+
Open the [showcase](http://localhost:5173/). The showcase uses base `bref-ui` Theme, Button, Surface, Pill and Icon components, default palette/styles, documentation fonts and the Bref logo. Each card uses an actual browser screenshot and links to the working site. Every demo provides a Showcase link; navigation between themes reloads the document to avoid retaining global CSS.
|
|
33
|
+
|
|
34
|
+
| Demo | Implemented interactions | Site | Theme demonstration |
|
|
35
|
+
| ------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------- |
|
|
36
|
+
| Tasks | Project creation/filtering, task status tabs, search, completion and task editing | [/relay](http://localhost:5173/relay) | [/themes/relay](http://localhost:5173/themes/relay) |
|
|
37
|
+
| House Shop | Category tabs, furniture sorting, product dialogs, cart quantities and simulated checkout | [/forma](http://localhost:5173/forma) | [/themes/forma](http://localhost:5173/themes/forma) |
|
|
38
|
+
| Site Reviewer | URL validation, simulated audit progress, history and finding review tabs | [/elsewhere](http://localhost:5173/elsewhere) | [/themes/elsewhere](http://localhost:5173/themes/elsewhere) |
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
**WIP:** VS Code, Liquid Glass and Minimalist Blocky are planned. They have no exports, demonstration routes or availability links.
|
|
45
|
+
|
|
46
|
+
## Library imports
|
|
47
|
+
|
|
48
|
+
Install the local tarball with `bun add ./bref-ui-themes-0.1.0.tgz`. Consumers need Svelte `^5.57.1` and Bref `^2.2.0` (peer dependencies). Fonts used by the themes are package dependencies.
|
|
49
|
+
|
|
50
|
+
```svelte
|
|
51
|
+
<script lang="ts">
|
|
52
|
+
import { Theme, Button } from 'bref-ui-themes/relay';
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<Theme>
|
|
56
|
+
<Button label="Create project" variant="filled" color="primary" />
|
|
57
|
+
</Theme>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The same API is available from each implemented theme:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { Theme, Button } from 'bref-ui-themes/forma';
|
|
64
|
+
// Or: import { Theme, Button } from 'bref-ui-themes/elsewhere';
|
|
65
|
+
// Namespace exports: import { Relay, Forma, Elsewhere } from 'bref-ui-themes';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each entry exports `Theme`, `Button`, `Checkbox`, `Dialog`, `Icon`, `Pill`, `PillChoiceGroup`, `PillGroup`, `Popover`, `Progress`, `Select`, `Spinner`, `Surface`, `Tabs` and `TextInput`. `Theme` takes an optional `children` snippet. Controls retain Bref's props, native attributes and applicable bindings; their types remain imported from `bref-ui/types`.
|
|
69
|
+
|
|
70
|
+
### Tabs
|
|
71
|
+
|
|
72
|
+
`Tabs` uses Bref 2.2.0's readonly item data, required icon names and bindable string value. The consumer renders the selected content:
|
|
73
|
+
|
|
74
|
+
```svelte
|
|
75
|
+
<script lang="ts">
|
|
76
|
+
import { Theme, Tabs } from 'bref-ui-themes/relay';
|
|
77
|
+
import type { TabItemDataProps } from 'bref-ui/types';
|
|
78
|
+
let view = $state('overview');
|
|
79
|
+
const items = [
|
|
80
|
+
{ id: 'overview', label: 'Overview', icon: 'dashboard' },
|
|
81
|
+
{ id: 'activity', label: 'Activity', icon: 'history' },
|
|
82
|
+
{ id: 'reports', label: 'Reports', icon: 'monitoring', disabled: true }
|
|
83
|
+
] as const satisfies readonly TabItemDataProps[];
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<Theme>
|
|
87
|
+
<Tabs {items} bind:value={view} label="Project views" />
|
|
88
|
+
<div role="tabpanel" aria-label={items.find((item) => item.id === view)?.label} tabindex="0">
|
|
89
|
+
{#if view === 'overview'}<p>8 tasks open, 3 completed.</p>
|
|
90
|
+
{:else}<p>The navigation review was completed today.</p>{/if}
|
|
91
|
+
</div>
|
|
92
|
+
</Theme>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Horizontal tabs accept `size="fit"` (default, scrolls on overflow) or `size="fill"`. Vertical tabs use `orientation="vertical"` without `size`. Optional `onChange`, `filled`, `transitionIn` and `transitionOut` retain the published API. Default indicator motion respects reduced motion; custom transitions should do the same. Arrow keys, Home and End move selection and skip disabled items. At small widths, horizontal tabs show the selected label, and vertical tabs show icons; accessible names remain available. Each theme demonstration includes fit, fill, vertical, disabled and bound-content examples.
|
|
96
|
+
|
|
97
|
+
These initial themes provide the demo palettes, fonts, common CSS and component styling. They use light mode and document-global Bref tokens/reset styles; mount one theme per document and use full-page navigation when changing themes. Site-specific layouts and content are separate from the library.
|
|
98
|
+
|
|
99
|
+
## Source layout
|
|
100
|
+
|
|
101
|
+
- `src/lib/<theme>/index.ts`: public theme entry; each site's demo and theme demonstration consume it.
|
|
102
|
+
- `src/lib/components`: Bref CLI-copied controls and their local dependencies, with `bref-ui/types` imports preserved.
|
|
103
|
+
- `src/lib/shared`: common reusable theme styling.
|
|
104
|
+
- `src/routes/(sites)`: complete Tasks, House Shop and Site Reviewer sites and the latest shared demo shell.
|
|
105
|
+
- `src/routes/(themes)`: demonstrations of implemented public theme exports.
|
|
106
|
+
- `src/routes/catalog`: showcase content adapted from the reusable gallery.
|
|
107
|
+
- `static/demos`: original photographic assets; `static/previews`: rendered catalog previews. Neither is included in the npm package.
|
|
108
|
+
|
|
109
|
+
The migration preserves the local checkbox motion/reduced-motion fix and animated dialog title/icon updates. Only demo-owned files were extracted from their source branches; the latest Relay shared shell was kept. See [verification.md](verification.md) for source revisions, validation and remaining limitations.
|
|
110
|
+
|
|
111
|
+
Tabs was copied from the npm-installed Bref 2.2.0 source using its registry, dependency planner and preview/install pipeline. Existing Icon and Theme dependencies were retained. The preview added only `tabs/tabs.svelte` and `tabs/item.svelte`, with explicit approval for the shared barrel merge; no existing component source was overwritten. Tabs types remain public imports from `bref-ui/types`.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '../icon/icon.svelte';
|
|
3
|
+
import { surface } from '../theme/surface.js';
|
|
4
|
+
import type { ButtonProps } from 'bref-ui/types';
|
|
5
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
color = 'foreground',
|
|
9
|
+
size = 'medium',
|
|
10
|
+
variant = 'neutral',
|
|
11
|
+
glass = false,
|
|
12
|
+
label,
|
|
13
|
+
icon,
|
|
14
|
+
trailingIcon,
|
|
15
|
+
wide = false,
|
|
16
|
+
rounded = false,
|
|
17
|
+
disabled = false,
|
|
18
|
+
href,
|
|
19
|
+
onClick,
|
|
20
|
+
class: className,
|
|
21
|
+
...attributes
|
|
22
|
+
}: ButtonProps = $props();
|
|
23
|
+
|
|
24
|
+
const isIcon = $derived(label === undefined);
|
|
25
|
+
const common = $derived({
|
|
26
|
+
class: ['button', surface({ variant, color, glass, hover: 'medium' }), className],
|
|
27
|
+
'aria-label': attributes['aria-label'] ?? (isIcon ? (icon?.label ?? icon?.name) : undefined),
|
|
28
|
+
'data-color': color,
|
|
29
|
+
'data-size': size,
|
|
30
|
+
'data-variant': variant,
|
|
31
|
+
'data-kind': isIcon ? 'icon' : 'normal',
|
|
32
|
+
'data-wide': wide && !isIcon ? '' : undefined,
|
|
33
|
+
'data-rounded': rounded && isIcon ? '' : undefined
|
|
34
|
+
});
|
|
35
|
+
const native = $derived(
|
|
36
|
+
href !== undefined
|
|
37
|
+
? {
|
|
38
|
+
href: disabled ? undefined : href,
|
|
39
|
+
'aria-disabled': disabled ? true : attributes['aria-disabled'],
|
|
40
|
+
tabindex: disabled ? -1 : attributes.tabindex
|
|
41
|
+
}
|
|
42
|
+
: { type: (attributes as HTMLButtonAttributes).type ?? 'button', disabled }
|
|
43
|
+
);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<svelte:element
|
|
47
|
+
this={href !== undefined ? 'a' : 'button'}
|
|
48
|
+
{...attributes}
|
|
49
|
+
{...common}
|
|
50
|
+
{...native}
|
|
51
|
+
onclick={disabled && href !== undefined ? (event: MouseEvent) => event.preventDefault() : onClick}
|
|
52
|
+
>
|
|
53
|
+
{#if icon}
|
|
54
|
+
<span><Icon {...icon} color={undefined} label={undefined} /></span>
|
|
55
|
+
{/if}
|
|
56
|
+
{#if !isIcon}
|
|
57
|
+
{label}
|
|
58
|
+
{#if trailingIcon}
|
|
59
|
+
<span><Icon {...trailingIcon} color={undefined} label={undefined} /></span>
|
|
60
|
+
{/if}
|
|
61
|
+
{/if}
|
|
62
|
+
</svelte:element>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.button {
|
|
66
|
+
--internal-height: max(40px, 2.5rem);
|
|
67
|
+
--internal-target: 24px;
|
|
68
|
+
--internal-padding: 1rem;
|
|
69
|
+
--internal-icon-size: 1.25rem;
|
|
70
|
+
--internal-font-size: 1rem;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
gap: 0.5rem;
|
|
76
|
+
min-height: max(var(--internal-height), var(--internal-target));
|
|
77
|
+
min-width: var(--internal-target);
|
|
78
|
+
padding: 0.25rem var(--internal-padding);
|
|
79
|
+
border-radius: 0.625rem;
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: var(--internal-font-size);
|
|
82
|
+
line-height: 1.25;
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
}
|
|
85
|
+
.button[data-size='x-small'] {
|
|
86
|
+
--internal-height: max(28px, 1.75rem);
|
|
87
|
+
--internal-padding: 0.625rem;
|
|
88
|
+
--internal-icon-size: 1rem;
|
|
89
|
+
--internal-font-size: 0.8125rem;
|
|
90
|
+
}
|
|
91
|
+
.button[data-size='small'] {
|
|
92
|
+
--internal-height: max(32px, 2rem);
|
|
93
|
+
--internal-padding: 0.75rem;
|
|
94
|
+
--internal-icon-size: 1.125rem;
|
|
95
|
+
--internal-font-size: 0.875rem;
|
|
96
|
+
}
|
|
97
|
+
.button[data-size='large'] {
|
|
98
|
+
--internal-height: max(48px, 3rem);
|
|
99
|
+
--internal-padding: 1.25rem;
|
|
100
|
+
--internal-icon-size: 1.5rem;
|
|
101
|
+
}
|
|
102
|
+
.button[data-size='x-large'] {
|
|
103
|
+
--internal-height: max(56px, 3.5rem);
|
|
104
|
+
--internal-padding: 1.5rem;
|
|
105
|
+
--internal-icon-size: 1.75rem;
|
|
106
|
+
--internal-font-size: 1.125rem;
|
|
107
|
+
}
|
|
108
|
+
.button[data-kind='icon'] {
|
|
109
|
+
width: max(var(--internal-height), var(--internal-target));
|
|
110
|
+
height: max(var(--internal-height), var(--internal-target));
|
|
111
|
+
padding: 0;
|
|
112
|
+
font-size: var(--internal-icon-size);
|
|
113
|
+
}
|
|
114
|
+
span {
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
font-size: var(--internal-icon-size);
|
|
119
|
+
line-height: 1;
|
|
120
|
+
}
|
|
121
|
+
.button[data-wide] {
|
|
122
|
+
width: 100%;
|
|
123
|
+
}
|
|
124
|
+
.button[data-rounded] {
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
}
|
|
127
|
+
.button:is(:disabled, [aria-disabled='true']) {
|
|
128
|
+
--surface-color: var(--color-muted, gray);
|
|
129
|
+
}
|
|
130
|
+
.button[data-variant='filled']:not(:disabled, [aria-disabled='true']):is(:hover, :focus-visible) {
|
|
131
|
+
filter: brightness(1.1);
|
|
132
|
+
}
|
|
133
|
+
.button[data-variant='filled']:not(:disabled, [aria-disabled='true']):is(
|
|
134
|
+
:active,
|
|
135
|
+
[aria-pressed='true'],
|
|
136
|
+
[data-surface-pressed='true']
|
|
137
|
+
) {
|
|
138
|
+
filter: brightness(0.85);
|
|
139
|
+
}
|
|
140
|
+
@media (pointer: coarse) {
|
|
141
|
+
.button {
|
|
142
|
+
--internal-target: 44px;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fly } from 'svelte/transition';
|
|
3
|
+
import { elasticOut } from 'svelte/easing';
|
|
4
|
+
import { prefersReducedMotion } from 'svelte/motion';
|
|
5
|
+
import Icon from '../icon/icon.svelte';
|
|
6
|
+
import type { CheckboxProps } from './types.js';
|
|
7
|
+
|
|
8
|
+
let { size = 'medium', checked = $bindable(), ...attributes }: CheckboxProps = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div data-checkbox data-size={size}>
|
|
12
|
+
<input {...attributes} type="checkbox" bind:checked />
|
|
13
|
+
{#if checked}
|
|
14
|
+
<span
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
transition:fly={{
|
|
17
|
+
duration: prefersReducedMotion.current ? 0 : 350,
|
|
18
|
+
y: prefersReducedMotion.current ? 0 : -6,
|
|
19
|
+
easing: elasticOut
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Icon name="check" style="font-variation-settings: 'wght' 500" />
|
|
23
|
+
</span>
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
div {
|
|
29
|
+
--internal-size: 1.25rem;
|
|
30
|
+
--internal-tint: var(--color-foreground, black);
|
|
31
|
+
--internal-strength-light: 4%;
|
|
32
|
+
--internal-strength-dark: 8%;
|
|
33
|
+
--internal-fill: light-dark(
|
|
34
|
+
color-mix(
|
|
35
|
+
in srgb,
|
|
36
|
+
var(--internal-tint) var(--internal-strength-light),
|
|
37
|
+
var(--color-background, white)
|
|
38
|
+
),
|
|
39
|
+
color-mix(
|
|
40
|
+
in srgb,
|
|
41
|
+
var(--internal-tint) var(--internal-strength-dark),
|
|
42
|
+
var(--color-background, white)
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
--internal-border: color-mix(
|
|
46
|
+
in srgb,
|
|
47
|
+
var(--color-foreground, black) 48%,
|
|
48
|
+
var(--color-background, white)
|
|
49
|
+
);
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
position: relative;
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
flex: 0 0 auto;
|
|
56
|
+
inline-size: var(--internal-size);
|
|
57
|
+
block-size: var(--internal-size);
|
|
58
|
+
border: 1px solid var(--internal-border);
|
|
59
|
+
border-radius: calc(var(--internal-size) * 0.2);
|
|
60
|
+
background: var(--internal-fill);
|
|
61
|
+
color: var(--color-foreground, black);
|
|
62
|
+
font-size: calc(var(--internal-size) * 0.8);
|
|
63
|
+
vertical-align: middle;
|
|
64
|
+
transition:
|
|
65
|
+
background-color 150ms ease,
|
|
66
|
+
border-color 150ms ease,
|
|
67
|
+
color 150ms ease;
|
|
68
|
+
}
|
|
69
|
+
input {
|
|
70
|
+
position: absolute;
|
|
71
|
+
z-index: 1;
|
|
72
|
+
inset: 0;
|
|
73
|
+
inline-size: 100%;
|
|
74
|
+
block-size: 100%;
|
|
75
|
+
margin: 0;
|
|
76
|
+
opacity: 0;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
span {
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
div[data-size='x-small'] {
|
|
84
|
+
--internal-size: 0.875rem;
|
|
85
|
+
}
|
|
86
|
+
div[data-size='small'] {
|
|
87
|
+
--internal-size: 1rem;
|
|
88
|
+
}
|
|
89
|
+
div[data-size='large'] {
|
|
90
|
+
--internal-size: 1.5rem;
|
|
91
|
+
}
|
|
92
|
+
div[data-size='x-large'] {
|
|
93
|
+
--internal-size: 1.75rem;
|
|
94
|
+
}
|
|
95
|
+
div:has(input:hover:not(:disabled):not(:checked)) {
|
|
96
|
+
--internal-strength-light: 8%;
|
|
97
|
+
--internal-strength-dark: 12%;
|
|
98
|
+
}
|
|
99
|
+
div:has(input:checked) {
|
|
100
|
+
--internal-tint: var(--color-primary, blue);
|
|
101
|
+
--internal-strength-light: 88%;
|
|
102
|
+
--internal-strength-dark: 88%;
|
|
103
|
+
--internal-border: var(--internal-fill);
|
|
104
|
+
color: var(--color-background, white);
|
|
105
|
+
}
|
|
106
|
+
div:has(input:checked:hover:not(:disabled)) {
|
|
107
|
+
--internal-strength-light: 92%;
|
|
108
|
+
--internal-strength-dark: 92%;
|
|
109
|
+
}
|
|
110
|
+
div:has(input:active:not(:disabled)) {
|
|
111
|
+
--internal-strength-light: 12%;
|
|
112
|
+
--internal-strength-dark: 16%;
|
|
113
|
+
}
|
|
114
|
+
div:has(input:checked:active:not(:disabled)) {
|
|
115
|
+
--internal-strength-light: 96%;
|
|
116
|
+
--internal-strength-dark: 96%;
|
|
117
|
+
}
|
|
118
|
+
div:has(input:user-invalid),
|
|
119
|
+
div:has(input[aria-invalid='true']) {
|
|
120
|
+
--internal-border: var(--color-error, #bc2436);
|
|
121
|
+
}
|
|
122
|
+
div:has(input:focus-visible) {
|
|
123
|
+
outline: 2px solid var(--color-primary, blue);
|
|
124
|
+
outline-offset: 3px;
|
|
125
|
+
}
|
|
126
|
+
div:has(input:disabled) {
|
|
127
|
+
opacity: 0.5;
|
|
128
|
+
cursor: not-allowed;
|
|
129
|
+
}
|
|
130
|
+
input:disabled {
|
|
131
|
+
cursor: inherit;
|
|
132
|
+
}
|
|
133
|
+
@media (prefers-reduced-motion: reduce) {
|
|
134
|
+
div {
|
|
135
|
+
transition: none;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
@media (forced-colors: active) {
|
|
139
|
+
div {
|
|
140
|
+
border-color: ButtonText;
|
|
141
|
+
background: Canvas;
|
|
142
|
+
color: CanvasText;
|
|
143
|
+
box-shadow: none;
|
|
144
|
+
-webkit-backdrop-filter: none;
|
|
145
|
+
backdrop-filter: none;
|
|
146
|
+
}
|
|
147
|
+
div:has(input:checked) {
|
|
148
|
+
background: Highlight;
|
|
149
|
+
color: HighlightText;
|
|
150
|
+
}
|
|
151
|
+
div:has(input:focus-visible) {
|
|
152
|
+
outline: 2px solid Highlight;
|
|
153
|
+
outline-offset: 3px;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { fly } from 'svelte/transition';
|
|
4
|
+
import { prefersReducedMotion } from 'svelte/motion';
|
|
5
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
6
|
+
import { surface } from '../theme/surface.js';
|
|
7
|
+
import Header from './header.svelte';
|
|
8
|
+
import Footer from './footer.svelte';
|
|
9
|
+
import type { DialogProps } from 'bref-ui/types';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
open = $bindable(false),
|
|
13
|
+
header,
|
|
14
|
+
children,
|
|
15
|
+
footer,
|
|
16
|
+
dismissible = true,
|
|
17
|
+
size = 'medium',
|
|
18
|
+
onclose,
|
|
19
|
+
oncancel,
|
|
20
|
+
...attributes
|
|
21
|
+
}: DialogProps = $props();
|
|
22
|
+
|
|
23
|
+
let dialog: HTMLDialogElement;
|
|
24
|
+
let returnFocus: HTMLElement | null = null;
|
|
25
|
+
const id = $props.id();
|
|
26
|
+
const smallScreen = new MediaQuery('(max-width: 36rem)', false);
|
|
27
|
+
const effectiveSize = $derived(smallScreen.current ? 'full-screen' : size);
|
|
28
|
+
|
|
29
|
+
const showModal = (node: HTMLElement) => {
|
|
30
|
+
const modal = node.closest('dialog');
|
|
31
|
+
if (!modal || modal.open) return;
|
|
32
|
+
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
33
|
+
modal.showModal();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const requestClose = () => {
|
|
37
|
+
if (dismissible) open = false;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleCancel = (event: Event & { currentTarget: EventTarget & HTMLDialogElement }) => {
|
|
41
|
+
oncancel?.(event);
|
|
42
|
+
const prevented = event.defaultPrevented;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
if (dismissible && !prevented) open = false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleClose = (event: Event & { currentTarget: EventTarget & HTMLDialogElement }) => {
|
|
48
|
+
open = false;
|
|
49
|
+
onclose?.(event);
|
|
50
|
+
tick().then(() => {
|
|
51
|
+
if (open && dialog?.isConnected) {
|
|
52
|
+
showModal(dialog);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (returnFocus?.isConnected) returnFocus.focus();
|
|
56
|
+
returnFocus = null;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<dialog
|
|
62
|
+
bind:this={dialog}
|
|
63
|
+
{...attributes}
|
|
64
|
+
data-size={effectiveSize}
|
|
65
|
+
data-visible={open}
|
|
66
|
+
aria-labelledby={`${id}-title`}
|
|
67
|
+
aria-describedby={header.description ? `${id}-description` : undefined}
|
|
68
|
+
oncancel={handleCancel}
|
|
69
|
+
onclose={handleClose}
|
|
70
|
+
>
|
|
71
|
+
{#if open}
|
|
72
|
+
<div
|
|
73
|
+
data-surface
|
|
74
|
+
class={surface({ color: 'background', glass: true, shadow: 'large' })}
|
|
75
|
+
use:showModal
|
|
76
|
+
transition:fly|global={{
|
|
77
|
+
y: prefersReducedMotion.current ? 0 : 16,
|
|
78
|
+
duration: prefersReducedMotion.current ? 0 : 300
|
|
79
|
+
}}
|
|
80
|
+
onoutroend={() => {
|
|
81
|
+
if (!open) dialog.close();
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Header {...header} {dismissible} {id} onDismiss={requestClose} />
|
|
85
|
+
<div data-content>{@render children()}</div>
|
|
86
|
+
{#if footer}<Footer {footer} />{/if}
|
|
87
|
+
</div>
|
|
88
|
+
{/if}
|
|
89
|
+
</dialog>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
dialog {
|
|
93
|
+
--internal-width: 42rem;
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
width: min(calc(100% - 2rem), var(--internal-width));
|
|
96
|
+
max-width: var(--internal-width);
|
|
97
|
+
max-height: calc(100dvh - 2rem);
|
|
98
|
+
margin: auto;
|
|
99
|
+
padding: 0;
|
|
100
|
+
border: 0;
|
|
101
|
+
background: transparent;
|
|
102
|
+
overflow: visible;
|
|
103
|
+
}
|
|
104
|
+
[data-surface] {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
max-height: calc(100dvh - 2rem);
|
|
108
|
+
border-radius: 1rem;
|
|
109
|
+
}
|
|
110
|
+
dialog[open] {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
}
|
|
114
|
+
dialog[data-size='x-small'] {
|
|
115
|
+
--internal-width: 20rem;
|
|
116
|
+
}
|
|
117
|
+
dialog[data-size='small'] {
|
|
118
|
+
--internal-width: 28rem;
|
|
119
|
+
}
|
|
120
|
+
dialog[data-size='large'] {
|
|
121
|
+
--internal-width: 56rem;
|
|
122
|
+
}
|
|
123
|
+
dialog[data-size='x-large'] {
|
|
124
|
+
--internal-width: 72rem;
|
|
125
|
+
}
|
|
126
|
+
dialog::backdrop {
|
|
127
|
+
background: color-mix(in srgb, var(--color-background, white) 48%, transparent);
|
|
128
|
+
-webkit-backdrop-filter: blur(0);
|
|
129
|
+
backdrop-filter: blur(0);
|
|
130
|
+
opacity: 0;
|
|
131
|
+
transition:
|
|
132
|
+
opacity 300ms ease,
|
|
133
|
+
-webkit-backdrop-filter 300ms ease,
|
|
134
|
+
backdrop-filter 300ms ease;
|
|
135
|
+
}
|
|
136
|
+
dialog[data-visible='true']::backdrop {
|
|
137
|
+
-webkit-backdrop-filter: blur(0.25rem);
|
|
138
|
+
backdrop-filter: blur(0.25rem);
|
|
139
|
+
opacity: 1;
|
|
140
|
+
}
|
|
141
|
+
@starting-style {
|
|
142
|
+
dialog[data-visible='true']::backdrop {
|
|
143
|
+
-webkit-backdrop-filter: blur(0);
|
|
144
|
+
backdrop-filter: blur(0);
|
|
145
|
+
opacity: 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
[data-content] {
|
|
149
|
+
min-height: 0;
|
|
150
|
+
padding: 1.25rem 1.75rem 1.5rem;
|
|
151
|
+
overflow: auto;
|
|
152
|
+
}
|
|
153
|
+
dialog[data-size='full-screen'] {
|
|
154
|
+
inset: 0.5rem;
|
|
155
|
+
width: calc(100% - 1rem);
|
|
156
|
+
height: calc(100dvh - 1rem);
|
|
157
|
+
max-width: none;
|
|
158
|
+
max-height: calc(100dvh - 1rem);
|
|
159
|
+
margin: 0;
|
|
160
|
+
}
|
|
161
|
+
dialog[data-size='full-screen'] > [data-surface] {
|
|
162
|
+
box-sizing: border-box;
|
|
163
|
+
height: 100%;
|
|
164
|
+
max-height: 100%;
|
|
165
|
+
}
|
|
166
|
+
dialog[data-size='full-screen'] [data-content] {
|
|
167
|
+
flex: 1;
|
|
168
|
+
}
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
dialog::backdrop {
|
|
171
|
+
transition: none;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
@media (forced-colors: active) {
|
|
175
|
+
[data-surface] {
|
|
176
|
+
border: 1px solid CanvasText;
|
|
177
|
+
}
|
|
178
|
+
dialog::backdrop {
|
|
179
|
+
background: Canvas;
|
|
180
|
+
-webkit-backdrop-filter: none;
|
|
181
|
+
backdrop-filter: none;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../button/button.svelte';
|
|
3
|
+
import type { DialogProps } from 'bref-ui/types';
|
|
4
|
+
|
|
5
|
+
let { footer }: { footer: NonNullable<DialogProps['footer']> } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<footer>
|
|
9
|
+
{#if typeof footer === 'function'}
|
|
10
|
+
{@render footer()}
|
|
11
|
+
{:else}
|
|
12
|
+
<Button {...footer.secondary} size="small" variant="neutral" />
|
|
13
|
+
<Button {...footer.primary} size="small" variant="filled" />
|
|
14
|
+
{/if}
|
|
15
|
+
</footer>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
footer {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: flex-end;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
gap: 0.75rem;
|
|
24
|
+
padding: 0 1.75rem 1.5rem;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DialogProps } from 'bref-ui/types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
footer: NonNullable<DialogProps['footer']>;
|
|
4
|
+
};
|
|
5
|
+
declare const Footer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type Footer = ReturnType<typeof Footer>;
|
|
7
|
+
export default Footer;
|