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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/dist/components/button/button.svelte +145 -0
  4. package/dist/components/button/button.svelte.d.ts +4 -0
  5. package/dist/components/checkbox/checkbox.svelte +156 -0
  6. package/dist/components/checkbox/checkbox.svelte.d.ts +4 -0
  7. package/dist/components/checkbox/types.d.ts +6 -0
  8. package/dist/components/checkbox/types.js +1 -0
  9. package/dist/components/dialog/dialog.svelte +184 -0
  10. package/dist/components/dialog/dialog.svelte.d.ts +4 -0
  11. package/dist/components/dialog/footer.svelte +26 -0
  12. package/dist/components/dialog/footer.svelte.d.ts +7 -0
  13. package/dist/components/dialog/header.svelte +66 -0
  14. package/dist/components/dialog/header.svelte.d.ts +4 -0
  15. package/dist/components/icon/icon.svelte +110 -0
  16. package/dist/components/icon/icon.svelte.d.ts +4 -0
  17. package/dist/components/index.d.ts +14 -0
  18. package/dist/components/index.js +14 -0
  19. package/dist/components/pill/content.svelte +30 -0
  20. package/dist/components/pill/content.svelte.d.ts +4 -0
  21. package/dist/components/pill/on-delete.svelte +55 -0
  22. package/dist/components/pill/on-delete.svelte.d.ts +4 -0
  23. package/dist/components/pill/pill.svelte +179 -0
  24. package/dist/components/pill/pill.svelte.d.ts +4 -0
  25. package/dist/components/pill-choice-group/pill-choice-group.svelte +150 -0
  26. package/dist/components/pill-choice-group/pill-choice-group.svelte.d.ts +4 -0
  27. package/dist/components/pill-group/container.svelte +40 -0
  28. package/dist/components/pill-group/container.svelte.d.ts +4 -0
  29. package/dist/components/pill-group/pill-group.svelte +29 -0
  30. package/dist/components/pill-group/pill-group.svelte.d.ts +4 -0
  31. package/dist/components/pill-group/types.d.ts +14 -0
  32. package/dist/components/pill-group/types.js +1 -0
  33. package/dist/components/popover/popover.svelte +111 -0
  34. package/dist/components/popover/popover.svelte.d.ts +4 -0
  35. package/dist/components/progress/progress.svelte +242 -0
  36. package/dist/components/progress/progress.svelte.d.ts +4 -0
  37. package/dist/components/select/empty.svelte +24 -0
  38. package/dist/components/select/empty.svelte.d.ts +4 -0
  39. package/dist/components/select/list.svelte +28 -0
  40. package/dist/components/select/list.svelte.d.ts +4 -0
  41. package/dist/components/select/option.svelte +78 -0
  42. package/dist/components/select/option.svelte.d.ts +4 -0
  43. package/dist/components/select/select.svelte +73 -0
  44. package/dist/components/select/select.svelte.d.ts +4 -0
  45. package/dist/components/select/trigger.svelte +109 -0
  46. package/dist/components/select/trigger.svelte.d.ts +4 -0
  47. package/dist/components/spinner/spinner.svelte +101 -0
  48. package/dist/components/spinner/spinner.svelte.d.ts +4 -0
  49. package/dist/components/surface/surface.svelte +101 -0
  50. package/dist/components/surface/surface.svelte.d.ts +4 -0
  51. package/dist/components/tabs/item.svelte +125 -0
  52. package/dist/components/tabs/item.svelte.d.ts +4 -0
  53. package/dist/components/tabs/tabs.svelte +124 -0
  54. package/dist/components/tabs/tabs.svelte.d.ts +4 -0
  55. package/dist/components/text-input/text-input.svelte +149 -0
  56. package/dist/components/text-input/text-input.svelte.d.ts +4 -0
  57. package/dist/components/theme/surface.d.ts +3 -0
  58. package/dist/components/theme/surface.js +11 -0
  59. package/dist/elsewhere/index.d.ts +2 -0
  60. package/dist/elsewhere/index.js +2 -0
  61. package/dist/elsewhere/theme.svelte +45 -0
  62. package/dist/elsewhere/theme.svelte.d.ts +9 -0
  63. package/dist/forma/index.d.ts +2 -0
  64. package/dist/forma/index.js +2 -0
  65. package/dist/forma/theme.svelte +44 -0
  66. package/dist/forma/theme.svelte.d.ts +9 -0
  67. package/dist/index.d.ts +3 -0
  68. package/dist/index.js +3 -0
  69. package/dist/relay/index.d.ts +2 -0
  70. package/dist/relay/index.js +2 -0
  71. package/dist/relay/theme.svelte +47 -0
  72. package/dist/relay/theme.svelte.d.ts +8 -0
  73. package/dist/shared/base.svelte +91 -0
  74. package/dist/shared/base.svelte.d.ts +26 -0
  75. 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
- # Temporary Holding Version
1
+ # bref-ui-themes
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ![Tasks rendered demo](static/previews/relay.jpg)
41
+ ![House Shop rendered demo](static/previews/forma.jpg)
42
+ ![Site Reviewer rendered demo](static/previews/elsewhere.jpg)
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,4 @@
1
+ import type { ButtonProps } from 'bref-ui/types';
2
+ declare const Button: import("svelte").Component<ButtonProps, {}, "">;
3
+ type Button = ReturnType<typeof Button>;
4
+ export default Button;
@@ -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,4 @@
1
+ import type { CheckboxProps } from './types.js';
2
+ declare const Checkbox: import("svelte").Component<CheckboxProps, {}, "checked">;
3
+ type Checkbox = ReturnType<typeof Checkbox>;
4
+ export default Checkbox;
@@ -0,0 +1,6 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { Size } from 'bref-ui/types';
3
+ export type CheckboxProps = Omit<HTMLInputAttributes, 'type' | 'size' | 'checked'> & {
4
+ size?: Size;
5
+ checked?: boolean;
6
+ };
@@ -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,4 @@
1
+ import type { DialogProps } from 'bref-ui/types';
2
+ declare const Dialog: import("svelte").Component<DialogProps, {}, "open">;
3
+ type Dialog = ReturnType<typeof Dialog>;
4
+ export default Dialog;
@@ -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;