@poodle64/ui 2026.9.16 → 2026.9.18
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 +124 -24
- package/dist/components/ui/app-shell/app-shell.svelte +2 -2
- package/dist/components/ui/app-shell/padding.d.ts +20 -0
- package/dist/components/ui/app-shell/padding.js +20 -0
- package/dist/components/ui/fact-grid/fact-grid.svelte +53 -0
- package/dist/components/ui/fact-grid/fact-grid.svelte.d.ts +18 -0
- package/dist/components/ui/fact-grid/index.d.ts +3 -0
- package/dist/components/ui/fact-grid/index.js +2 -0
- package/dist/components/ui/info-tip/info-tip.svelte +18 -1
- package/dist/components/ui/page-canvas/index.d.ts +3 -0
- package/dist/components/ui/page-canvas/index.js +2 -0
- package/dist/components/ui/page-canvas/page-canvas.svelte +374 -0
- package/dist/components/ui/page-canvas/page-canvas.svelte.d.ts +23 -0
- package/dist/components/ui/page-canvas/types.d.ts +35 -0
- package/dist/components/ui/page-canvas/types.js +8 -0
- package/dist/components/ui/panel/panel.svelte +97 -5
- package/dist/components/ui/panel/panel.svelte.d.ts +36 -1
- package/dist/components/ui/settings-shell/index.d.ts +2 -0
- package/dist/components/ui/settings-shell/index.js +2 -0
- package/dist/components/ui/settings-shell/settings-shell.svelte +111 -0
- package/dist/components/ui/settings-shell/settings-shell.svelte.d.ts +78 -0
- package/dist/styles.css +68 -13
- package/package.json +1 -1
- package/registry/component-map.json +40 -5
- package/registry/component-map.md +19 -2
package/README.md
CHANGED
|
@@ -85,30 +85,31 @@ app — the page chrome is. These are the cross-cutting surfaces every route
|
|
|
85
85
|
composes from, so a household app gets its layout language from the package
|
|
86
86
|
rather than rebuilding it:
|
|
87
87
|
|
|
88
|
-
| Import | What it is
|
|
89
|
-
| ----------------------------------------------- |
|
|
90
|
-
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar.
|
|
91
|
-
| `panel` | The generic titled card: optional icon, subtitle
|
|
92
|
-
| `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions.
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `dialog
|
|
96
|
-
| `
|
|
97
|
-
| `stat-
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
88
|
+
| Import | What it is |
|
|
89
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar. |
|
|
91
|
+
| `panel` | The generic titled card, and the settings section: optional icon, a clamped one-line `subtitle` or a wrapping `description`, trailing `action`s, a body that can opt out of padding, an optional `footer` strip for Save/Cancel, and `tone="destructive"` for a danger zone. There is no `SettingsSection`; see [The settings destination](#the-settings-destination). |
|
|
92
|
+
| `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions. |
|
|
93
|
+
| `settings-shell` | The settings destination: a grouped section list of `NavItem` rows beside the section you are reading, both scrolling independently from `md`, stacked below it. Render it inside `AppShell` with `padded={false}`. |
|
|
94
|
+
| `context-column` | The persistent right-hand column: a standing `StatList` plus an optional detail that flows in on select. Below `xl` (1280px), where the column has nowhere to sit, the same content opens from a floating trigger instead of disappearing. |
|
|
95
|
+
| `app-dialog` | The dialogue frame: titled header, scrollable body, footer action bar, five sizes (`xs`…`xl`), and an `onOpenChange` for the dismissals the caller did not drive. |
|
|
96
|
+
| `dialog-section` | One section of a dialogue body; adjacent sections are divided automatically. |
|
|
97
|
+
| `stat-card` | A single metric that earns its space (label, value, unit, sub, status dot, and `valueTone` to colour the figure itself). |
|
|
98
|
+
| `stat-list` | A route's low-context integers as a label→value list. Zero-aware: `muted` keeps a healthy zero quiet. |
|
|
99
|
+
| `arc-gauge` | A radial capacity/percentage ring for a single 0–100 metric, in a footprint too compact for a `stat-card`. |
|
|
100
|
+
| `bar-row` | A labelled horizontal bar with a trailing tabular value, for a ranked list (usage, rank, token burn). |
|
|
101
|
+
| `scorecard` | A compact 0/1/2 dot-row health strip for several independent checks read at a glance. |
|
|
102
|
+
| `sparkline` | An inline multi-series area+line trend for a row or card with room for a trend but not a full chart. |
|
|
103
|
+
| `status` / `status-badge` | The fixed five-state vocabulary (`success \| warning \| error \| info \| neutral`) and the one state chip, with `pulse` for a state still in motion and `class` for placement. `status-badge` alone also accepts `'primary'`, a brand-emphasis extension outside the shared vocabulary — `stat-card`, `stat-list` and `data-table-toolbar` never see it. |
|
|
104
|
+
| `empty-state` / `error-state` / `loading-state` | The shared blank, error and loading surfaces. Never hand-roll one. |
|
|
105
|
+
| `info-tip` | One tooltip pattern: a small info trigger, or wrap an existing affordance as children. |
|
|
106
|
+
| `data-table-toolbar` | Search field plus filter-chip groups for a TanStack table. Owns no state; fires callbacks. |
|
|
107
|
+
| `schema-form` | The renderer for a config object the server described: a JSON Schema plus a JSON Forms UI Schema in, a form out. Anything it cannot dispatch renders flagged, never blank; see [Server-described forms](#server-described-forms) below. |
|
|
108
|
+
| `data-table-tanstack` | The TanStack-backed table: global search, column filters, master-detail row select, opt-in bulk selection, responsive column hiding, a first-class empty branch. |
|
|
109
|
+
| `library-browse` | The faceted catalogue index: search, facet rail, active filter chips, the document table and a pager, all over plain props (`LibraryDocument[]`, `LibraryFacet[]`). The page fetches, maps and routes; the component renders. Library data is fixed components because its shape is stable; configuration is `schema-form` because its shape changes (#30). |
|
|
110
|
+
| `collection-detail` | One collection's surface: identity (`detail-panel`), an at-a-glance `stat-list`, and the documents it holds, with `actions` and `children` slots for the app-specific rest. |
|
|
111
|
+
| `document-detail` | One document's surface: identity fields, locations, tags and collection memberships, each section present exactly when its data is. Membership links are the app's own via `collectionHref`. |
|
|
112
|
+
| `search-results` | A ranked retrieval answer: title, the matched passage with accent-tinted highlights, source chip, mapped state and a mono relevance figure per hit; plus the before-any-search and matched-nothing empties. |
|
|
112
113
|
|
|
113
114
|
**The application shell** (`app-shell`, `command-palette`). Page chrome is not
|
|
114
115
|
what makes an app feel like an app either — the shell is. Five household
|
|
@@ -302,6 +303,105 @@ palette:
|
|
|
302
303
|
`AppNav` used outside the chrome (a navigation list inside a page) keeps the
|
|
303
304
|
page's ink, so inverting the rail does not drag it along.
|
|
304
305
|
|
|
306
|
+
### The settings destination
|
|
307
|
+
|
|
308
|
+
Settings is the one destination with a second column, and it takes one shape in
|
|
309
|
+
every app. `SettingsShell` is that shape: a grouped section list beside the
|
|
310
|
+
section you are reading, both panes scrolling independently from `md`.
|
|
311
|
+
|
|
312
|
+
```svelte
|
|
313
|
+
<script lang="ts">
|
|
314
|
+
import { page } from '$app/state';
|
|
315
|
+
import AppShell from '@poodle64/ui/app-shell';
|
|
316
|
+
import type { NavSource } from '@poodle64/ui/app-shell';
|
|
317
|
+
import SettingsShell from '@poodle64/ui/settings-shell';
|
|
318
|
+
import Users from '@lucide/svelte/icons/users';
|
|
319
|
+
import Gavel from '@lucide/svelte/icons/gavel';
|
|
320
|
+
|
|
321
|
+
// Grouped by WHOSE setting it is: the person's, then the workspace's. It is
|
|
322
|
+
// the only split a reader can predict: "yours" changes what you see, "this
|
|
323
|
+
// company" changes what everyone sees. A group with no items renders nothing,
|
|
324
|
+
// so an app with no personal sections yet can leave it in the list.
|
|
325
|
+
const sections: NavSource = [
|
|
326
|
+
{ heading: 'Yours', items: [] },
|
|
327
|
+
{
|
|
328
|
+
heading: 'This company',
|
|
329
|
+
items: [
|
|
330
|
+
{ label: 'Users', href: '/settings/users', icon: Users },
|
|
331
|
+
{ label: 'Delegation limits', href: '/settings/delegation-limits', icon: Gavel }
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
];
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<!-- padded={false}: the list's rule and tint have to reach the content area's
|
|
338
|
+
edges, and SettingsShell pads its own content pane to the same rhythm. -->
|
|
339
|
+
<AppShell {nav} currentPath={page.url.pathname} settingsHref="/settings" padded={false}>
|
|
340
|
+
<SettingsShell {sections} currentPath={page.url.pathname}>
|
|
341
|
+
{@render children()}
|
|
342
|
+
</SettingsShell>
|
|
343
|
+
</AppShell>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Prop | Purpose |
|
|
347
|
+
| ------------- | ------------------------------------------------------------------------------ |
|
|
348
|
+
| `sections` | Groups of `NavItem`, bare items, or a mix: the rail's own vocabulary. |
|
|
349
|
+
| `currentPath` | Which row is the page. Prefix-matched on the segment boundary, as the rail is. |
|
|
350
|
+
| `listLabel` | The list's accessible name. Defaults to `Settings sections`. |
|
|
351
|
+
|
|
352
|
+
A row at the settings ROOT (`/settings`) needs `exact: true`, or its prefix match
|
|
353
|
+
keeps it lit on every section beneath it alongside the section's own row.
|
|
354
|
+
|
|
355
|
+
Three things are settled here and are not props:
|
|
356
|
+
|
|
357
|
+
**Rows are icon plus label, with nothing beneath.** A description long enough to
|
|
358
|
+
be worth reading does not fit a 240px column; every one of them truncated to an
|
|
359
|
+
ellipsis and told the reader less than the label already did.
|
|
360
|
+
|
|
361
|
+
**There is no "Settings" heading above the list.** The bar already names the
|
|
362
|
+
section you are on, so a heading here is the same word twice.
|
|
363
|
+
|
|
364
|
+
**Below `md` the list stacks above the content and the page scrolls as one.** A
|
|
365
|
+
horizontally scrollable strip of the same rows was the alternative and loses on
|
|
366
|
+
the requirement it was meant to serve: it fits about two and a half rows at
|
|
367
|
+
375px, so the rest are behind a sideways swipe with no scrollbar advertising
|
|
368
|
+
them — a swipe _then_ a tap, where a stack is a scroll the reader is already
|
|
369
|
+
doing. It also has to drop the group labels for width.
|
|
370
|
+
|
|
371
|
+
Density works here as it does in the rail, because the list _is_ the rail's
|
|
372
|
+
list: the controls inside a section ride the `--ds-control-*` ramp and shrink
|
|
373
|
+
under `data-ds-density="compact"`, while a section row keeps `AppNav`'s own
|
|
374
|
+
36px whatever the density. A row that shrank here and not in the rail would be
|
|
375
|
+
the drift, not the fix.
|
|
376
|
+
|
|
377
|
+
#### The sections themselves are `Panel`s
|
|
378
|
+
|
|
379
|
+
There is no `SettingsSection`, and deliberately so: it would have been `Panel`
|
|
380
|
+
with a different name (same surface, same header, same title), and the estate
|
|
381
|
+
would then hold two answers to "a titled section of a route", which is the drift
|
|
382
|
+
this package exists to end. What settings actually needed was three props, and
|
|
383
|
+
all three serve any route:
|
|
384
|
+
|
|
385
|
+
```svelte
|
|
386
|
+
<Panel title="Delegation limits"
|
|
387
|
+
description="Who can approve a bill, and up to what value.">
|
|
388
|
+
<!-- controls -->
|
|
389
|
+
{#snippet footer()}
|
|
390
|
+
<Button variant="ghost" size="sm">Cancel</Button>
|
|
391
|
+
<Button size="sm">Save</Button>
|
|
392
|
+
{/snippet}
|
|
393
|
+
</Panel>
|
|
394
|
+
|
|
395
|
+
<Panel title="Delete this workspace" tone="destructive" icon={Trash2}
|
|
396
|
+
description="Every project, document and filing decision goes with it.">
|
|
397
|
+
<Button variant="destructive" size="sm">Delete</Button>
|
|
398
|
+
</Panel>
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`description` wraps; `subtitle` is the one-line qualifier beside the title and
|
|
402
|
+
truncates. Carry one or the other. A page writes a run of `Panel`s and no
|
|
403
|
+
wrapper: the pane stacks them on the package's section rhythm itself.
|
|
404
|
+
|
|
305
405
|
### The content measure
|
|
306
406
|
|
|
307
407
|
How wide the page body may get, chosen once in the layout from a scale of four:
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
*/
|
|
80
80
|
import type { Snippet } from 'svelte';
|
|
81
81
|
import type { ShellMeasure } from './measure.js';
|
|
82
|
+
import { CONTENT_PADDING } from './padding.js';
|
|
82
83
|
import type { ShellTexture } from './texture.js';
|
|
83
84
|
import { toggleMode } from 'mode-watcher';
|
|
84
85
|
import Menu from '@lucide/svelte/icons/menu';
|
|
@@ -917,8 +918,7 @@
|
|
|
917
918
|
'flex min-h-0 min-w-0 flex-1 flex-col',
|
|
918
919
|
'w-full',
|
|
919
920
|
capped && 'ds-shell-measure',
|
|
920
|
-
padded &&
|
|
921
|
-
'px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12'
|
|
921
|
+
padded && CONTENT_PADDING
|
|
922
922
|
)}
|
|
923
923
|
data-measure={capped ?? undefined}
|
|
924
924
|
>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page padding the shell gives its content area.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than a literal in app-shell.svelte, because it is no longer
|
|
5
|
+
* declared in one place: SettingsShell runs inside an UNPADDED content area (its
|
|
6
|
+
* section list has to reach the area's edges to read as a pane rather than a
|
|
7
|
+
* floating card) and then pads its own content pane, which must land on the same
|
|
8
|
+
* rhythm as every other page or the settings destination is the one route whose
|
|
9
|
+
* text sits somewhere else.
|
|
10
|
+
*
|
|
11
|
+
* Utilities rather than a class in styles.css, deliberately: both consumers pass
|
|
12
|
+
* this through `cn()`, so an app overriding the padding — `mainClass="px-0"`, a
|
|
13
|
+
* flush table — still resolves through tailwind-merge. A base-layer class would
|
|
14
|
+
* be invisible to it, and the utility would win by layer order instead of by
|
|
15
|
+
* being last, which is the cascade trap recorded against `.ds-nav`'s ink.
|
|
16
|
+
*
|
|
17
|
+
* The bottom pad carries `env(safe-area-inset-bottom)` so the last row of a page
|
|
18
|
+
* clears a home-indicator gesture bar.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CONTENT_PADDING = "px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page padding the shell gives its content area.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than a literal in app-shell.svelte, because it is no longer
|
|
5
|
+
* declared in one place: SettingsShell runs inside an UNPADDED content area (its
|
|
6
|
+
* section list has to reach the area's edges to read as a pane rather than a
|
|
7
|
+
* floating card) and then pads its own content pane, which must land on the same
|
|
8
|
+
* rhythm as every other page or the settings destination is the one route whose
|
|
9
|
+
* text sits somewhere else.
|
|
10
|
+
*
|
|
11
|
+
* Utilities rather than a class in styles.css, deliberately: both consumers pass
|
|
12
|
+
* this through `cn()`, so an app overriding the padding — `mainClass="px-0"`, a
|
|
13
|
+
* flush table — still resolves through tailwind-merge. A base-layer class would
|
|
14
|
+
* be invisible to it, and the utility would win by layer order instead of by
|
|
15
|
+
* being last, which is the cascade trap recorded against `.ds-nav`'s ink.
|
|
16
|
+
*
|
|
17
|
+
* The bottom pad carries `env(safe-area-inset-bottom)` so the last row of a page
|
|
18
|
+
* clears a home-indicator gesture bar.
|
|
19
|
+
*/
|
|
20
|
+
export const CONTENT_PADDING = 'px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface Fact {
|
|
3
|
+
/** Stable identity for #each; never rendered. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Uppercase eyebrow label. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The value. A long one (a sentence, an address) spans the row. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Render the value in the figures' face: mono, tabular. */
|
|
10
|
+
numeric?: boolean;
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
16
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
17
|
+
|
|
18
|
+
// A document's key facts read at a glance, INSIDE whatever card already
|
|
19
|
+
// holds them — eyebrow label over value, wrapped into a responsive grid,
|
|
20
|
+
// with no border or title of its own. StatList is this same situation as
|
|
21
|
+
// a titled card for the context column; reach for FactGrid instead where
|
|
22
|
+
// a Panel or DetailPanel already supplies the box, so nesting never
|
|
23
|
+
// doubles the border (godswood's FactGrid stop-gap, #839).
|
|
24
|
+
let {
|
|
25
|
+
facts,
|
|
26
|
+
class: klass = '',
|
|
27
|
+
ref = $bindable(null),
|
|
28
|
+
...restProps
|
|
29
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
30
|
+
facts: Fact[];
|
|
31
|
+
} = $props();
|
|
32
|
+
|
|
33
|
+
// A sentence takes the whole row; a short value shares it with its
|
|
34
|
+
// neighbours. 40 is the length the source stop-gap measured this against.
|
|
35
|
+
const LONG_VALUE = 40;
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<dl
|
|
39
|
+
bind:this={ref}
|
|
40
|
+
class={cn('grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3 xl:grid-cols-4', klass)}
|
|
41
|
+
{...restProps}
|
|
42
|
+
>
|
|
43
|
+
{#each facts as fact (fact.key)}
|
|
44
|
+
<div class={cn('min-w-0', fact.value.length > LONG_VALUE && 'col-span-full')}>
|
|
45
|
+
<dt class="text-muted-foreground text-2xs tracking-eyebrow font-medium uppercase">
|
|
46
|
+
{fact.label}
|
|
47
|
+
</dt>
|
|
48
|
+
<dd class={cn('mt-1 break-words', fact.numeric && 'font-mono tabular-nums')}>
|
|
49
|
+
{fact.value}
|
|
50
|
+
</dd>
|
|
51
|
+
</div>
|
|
52
|
+
{/each}
|
|
53
|
+
</dl>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Fact {
|
|
2
|
+
/** Stable identity for #each; never rendered. */
|
|
3
|
+
key: string;
|
|
4
|
+
/** Uppercase eyebrow label. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The value. A long one (a sentence, an address) spans the row. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Render the value in the figures' face: mono, tabular. */
|
|
9
|
+
numeric?: boolean;
|
|
10
|
+
}
|
|
11
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
12
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
13
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
14
|
+
facts: Fact[];
|
|
15
|
+
};
|
|
16
|
+
declare const FactGrid: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
17
|
+
type FactGrid = ReturnType<typeof FactGrid>;
|
|
18
|
+
export default FactGrid;
|
|
@@ -22,7 +22,24 @@
|
|
|
22
22
|
|
|
23
23
|
<Tooltip.Provider delayDuration={150}>
|
|
24
24
|
<Tooltip.Root>
|
|
25
|
-
|
|
25
|
+
<!--
|
|
26
|
+
The icon trigger takes a 44x44 hit area (`size-11`) and hands the space
|
|
27
|
+
straight back (`-m-3.5`), so the glyph, the row it sits in and the
|
|
28
|
+
spacing around it are all exactly as before and only a thumb can tell
|
|
29
|
+
the difference. Measured on one consumer at 390 px: this button was
|
|
30
|
+
16x16 on every page header carrying `info`, well under Apple's 44 pt
|
|
31
|
+
floor, and it is the package's button on every consumer's page.
|
|
32
|
+
|
|
33
|
+
Only the bare icon form gets it. Where `children` are passed the trigger
|
|
34
|
+
wraps an affordance the app already sized — a status dot inline in a
|
|
35
|
+
sentence, a chip in a row — and a 44 px box around one of those moves
|
|
36
|
+
the layout it was placed in.
|
|
37
|
+
-->
|
|
38
|
+
<Tooltip.Trigger
|
|
39
|
+
class="inline-flex cursor-help items-center align-middle {children
|
|
40
|
+
? ''
|
|
41
|
+
: '-m-3.5 size-11 justify-center'}"
|
|
42
|
+
>
|
|
26
43
|
{#if children}
|
|
27
44
|
{@render children()}
|
|
28
45
|
{:else}
|