Merge pull request #1807 from projectsend/feat/package-styling-hooks

Let an installed package restyle the staff area and supply its own browser icons
This commit is contained in:
Ignacio Nelson
2026-10-02 15:44:52 -03:00
committed by GitHub
11 changed files with 36 additions and 12 deletions
+5
View File
@@ -1,5 +1,10 @@
import '../css/app.css';
// Stylesheets an installed package ships under resources/css, the styling
// counterpart of the package pages resolved below. Imported after app.css so
// a package can restyle what core draws; core names no package and no style.
import.meta.glob('../../vendor/*/*/resources/css/*.css', { eager: true });
import { createInertiaApp, router } from '@inertiajs/react';
import axios from 'axios';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
+3 -1
View File
@@ -7,7 +7,9 @@ export default function AppLogoIcon(props: SVGAttributes<SVGElement>) {
const gradientId = useId();
return (
<svg {...props} viewBox="0 0 234.26482 252.25172" xmlns="http://www.w3.org/2000/svg">
// Marked so an installed package can restyle the default mark; only
// drawn when no logo was uploaded in Branding, which always wins.
<svg data-slot="app-logo-default" {...props} viewBox="0 0 234.26482 252.25172" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id={gradientId} gradientUnits="userSpaceOnUse" x1="31.5263" y1="211.0108" x2="197.4637" y2="50.766899">
<stop offset="0.1675" stopColor="#5219B3" />
+6 -2
View File
@@ -18,11 +18,15 @@ export function AppShell({ children, variant = 'header' }: AppShellProps) {
};
if (variant === 'header') {
return <div className="flex min-h-screen w-full flex-col">{children}</div>;
return (
<div data-surface="staff" className="flex min-h-screen w-full flex-col">
{children}
</div>
);
}
return (
<SidebarProvider defaultOpen={isOpen} open={isOpen} onOpenChange={handleSidebarChange}>
<SidebarProvider data-surface="staff" defaultOpen={isOpen} open={isOpen} onOpenChange={handleSidebarChange}>
{children}
</SidebarProvider>
);
@@ -9,7 +9,10 @@ import { type BreadcrumbItem as BreadcrumbItemType } from '@/types';
export function AppSidebarHeader({ breadcrumbs = [] }: { breadcrumbs?: BreadcrumbItemType[] }) {
return (
<header className="border-sidebar-border/50 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4">
<header
data-slot="app-header"
className="border-sidebar-border/50 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
>
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Breadcrumbs breadcrumbs={breadcrumbs} />
@@ -23,6 +23,7 @@ export function WidgetBox({ id, title, headerExtra, children }: { id: string; ti
<div
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
data-slot="card"
className={`bg-card rounded-lg border p-4 ${isDragging ? 'z-10 opacity-50' : ''}`}
>
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
+1 -1
View File
@@ -12,7 +12,7 @@ export function ListToolbar({ children, showClear, onClear }: { children: ReactN
const { t } = useTranslation();
return (
<div className="mb-4 flex flex-wrap items-end gap-3 rounded-lg border p-4">
<div data-slot="list-toolbar" className="mb-4 flex flex-wrap items-end gap-3 rounded-lg border p-4">
{children}
{showClear && (
<Button type="button" variant="ghost" onClick={onClear}>
+3 -1
View File
@@ -9,7 +9,9 @@ export default function ProjectSendLogo(props: SVGAttributes<SVGElement>) {
const gradientId = useId();
return (
<svg {...props} viewBox="0 0 1046.5 257" xmlns="http://www.w3.org/2000/svg">
// Marked so an installed package can restyle the default wordmark;
// only drawn when no logo was uploaded in Branding.
<svg data-slot="app-wordmark-default" {...props} viewBox="0 0 1046.5 257" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id={gradientId} gradientUnits="userSpaceOnUse" x1="31.5263" y1="211.0108" x2="197.4637" y2="50.766899">
<stop offset="0.1675" stopColor="#5219B3" />
+1 -1
View File
@@ -24,7 +24,7 @@ interface TableShellProps {
*/
export function TableShell({ columns, emptyMessage, isEmpty, children }: TableShellProps) {
return (
<div className="overflow-x-auto rounded-lg border">
<div data-slot="table-shell" className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b text-left">
+1 -1
View File
@@ -36,7 +36,7 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
return <Comp data-slot="button" data-variant={variant ?? 'default'} className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
});
Button.displayName = 'Button';
+1 -1
View File
@@ -3,7 +3,7 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-xs', className)} {...props} />
<div ref={ref} data-slot="card" className={cn('rounded-lg border bg-card text-card-foreground shadow-xs', className)} {...props} />
));
Card.displayName = 'Card';
+10 -3
View File
@@ -29,9 +29,16 @@
there is nothing on the client that could work the name out. --}}
<meta name="xsrf-cookie" content="{{ \App\Http\Middleware\ValidateCsrfToken::cookieName() }}">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
{{-- An installed package may name its own icons in
projectsend.icons; they then replace these as a set, so a
stray default never outranks one of them in some browser. --}}
@forelse (config('projectsend.icons', []) as $icon)
<link rel="{{ $icon['rel'] }}" href="{{ $icon['href'] }}"@isset($icon['type']) type="{{ $icon['type'] }}"@endisset @isset($icon['sizes']) sizes="{{ $icon['sizes'] }}"@endisset>
@empty
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
@endforelse
@routes
@viteReactRefresh