Search by

Renderers for Storyfeed activity feeds: Blade, Vue/Inertia and React/Inertia component kits.

Package info

github.com/storyfeed/ui

pkg:composer/storyfeed/ui

Fund package maintenance!

storyfeed

jaspertey

Statistics

Installs: 3 812

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.0 2026-10-01 16:28 UTC

This package is auto-updated.

Last update: 2026-10-08 04:06:24 UTC


README

GitHub Tests Action Status

Storyfeed UI is a free, MIT-licensed set of renderers for Storyfeed activity feeds: components that take a page of the feed and draw it. Blade, Vue/Inertia and React/Inertia, with Livewire following.

It works the way Laravel's pagination does. Core hands you the data, a FeedPage whose items read as Storyfeed\Support\FeedItem, and this package renders it with default Blade views you can publish and restyle.

The data forms moved to core. v0.1.1's Storyfeed\Ui\Data\* classes are core's body types, Storyfeed\Body\* in storyfeed/storyfeed 0.11, stored as Storyfeed/Body/Excerpt. A form's name must not contain the library that defined it, so the vocabulary was always core's. This package is the renderers. See the changelog for the upgrade.

Early development. Storyfeed is pre-1.0. Minor releases can change the API without a deprecation cycle. Commit your application's Composer lockfile to keep installations reproducible.

Installation

Requires PHP 8.4 or later in the PHP 8 series. The current test harness uses Laravel 12; CI runs PHP 8.4 and 8.5 on Ubuntu and Windows with lowest and stable dependencies.

composer require storyfeed/ui

The service provider registers itself through package discovery.

The Blade, Vue and React kits use Tailwind CSS v4 and Laravel starter-kit colour tokens. No Typography plugin or package stylesheet is needed. Register the package's views in your application's resources/css/app.css file:

@source "../../vendor/storyfeed/ui/resources/views";

Compile your application's CSS with npm run build. Your layout must load the compiled CSS, for example with @vite('resources/css/app.css').

Vue (Inertia)

Copy the Vue 3 kit into your app:

php artisan storyfeed:ui vue
npm install lucide-vue-next markdown-it sanitize-html

The default destination is resources/js/components/storyfeed/; change it with --path=resources/js/my-feed. These are TypeScript <script setup> components for Tailwind v4. They expect the Laravel Vue starter-kit colour tokens, including background, foreground, muted, muted-foreground, card, primary, primary-foreground, border and ring. Tokens supply dark mode. The Vue kit does not need the Typography plugin or a separate stylesheet.

If the copied directory is not already scanned, add this once to resources/css/app.css (paths are relative to that CSS file):

@source "../js/components/storyfeed";
<script setup lang="ts">
import FeedStream from '@/components/storyfeed/FeedStream.vue';
import type { FeedNode } from '@/components/storyfeed/types';
defineProps<{ items: FeedNode[] }>();
</script>

<template>
    <FeedStream :items="items" />
</template>

Copied files belong to your app and may be edited. Re-running writes missing files and skips identical ones. Files that differ are kept and reported; php artisan storyfeed:ui vue --diff prints unified diffs to reconcile by hand. Use --force to replace differing files. Diffs are generated by the command; no external diff tool is required. The summary reports written, unchanged and differing files. See the kit README for slots, rail options, Component bodies and Inertia links.

React (Inertia)

Copy the React 19 kit, including its shared TypeScript core:

php artisan storyfeed:ui react
npm install react@^19 react-dom@^19 lucide-react markdown-it sanitize-html
npm install -D @types/react @types/react-dom @types/markdown-it @types/sanitize-html

The default destination, --path, --diff and --force semantics are the same as Vue's. Both commands include a self-contained shared/ directory. Add @source "../js/components/storyfeed"; in resources/css/app.css if needed. React uses the same Tailwind v4 starter-kit tokens as Vue and Blade.

import { Link } from '@inertiajs/react';
import { FeedProvider, FeedStream } from '@/components/storyfeed';
import type { FeedPayload } from '@/components/storyfeed';

export default function History({ feed }: { feed: FeedPayload }) {
    return <FeedProvider FEED_LINK={Link}>
        <FeedStream items={feed.items} nextCursor={feed.next_cursor}
            onLoadMore={() => { /* load and append the next page in your app */ }} />
    </FeedProvider>;
}

Function components work with Inertia 2/3's React adapter and the Laravel React starter kit. FeedProvider supplies the FEED_LINK, FEED_COMPONENTS, FEED_NOW, FEED_FILE_LABELLER and FEED_MEDIA_OBJECT_PLACEMENT seams. body, annotations and time render props replace Vue slots. Groups use native <details>. The initial SSR/hydration render uses stable ISO dates; after mount, labels use the browser's local calendar and live or pinned clock. See the React kit README for the complete provider, pagination, body and SSR contracts.

Usage

Pass a page of the feed to a view:

use Illuminate\Http\Request;
use Storyfeed\Facades\Storyfeed;

Route::get('/', fn (Request $request) => view('feed', [
    'page' => Storyfeed::feed()->cursor($request->query('cursor'))->get(),
]));

Render the page with the feed component:

<x-storyfeed::feed :page="$page" />

Attributes on the tag, such as class, land on the feed's root element. The empty slot replaces the words shown for an empty page:

<x-storyfeed::feed :page="$page">
    <x-slot:empty>Nothing has happened yet.</x-slot:empty>
</x-storyfeed::feed>

Components

<x-storyfeed::feed> is built from smaller anonymous components, and each one can be used on its own:

Component Draws
<x-storyfeed::feed :page> the page, then a link to older activity
<x-storyfeed::item :item> one item: an activity or a group
<x-storyfeed::activity :activity> an activity row
<x-storyfeed::group :group> a group row, its members behind a disclosure
<x-storyfeed::headline :headline> a headline, each entity linked
<x-storyfeed::glyph :glyph :intent> the icon disc
<x-storyfeed::time :at> when it happened
<x-storyfeed::media :image> a picture
<x-storyfeed::body :body> one body, by its type
<x-storyfeed::pager :cursor> the link to the next page

Each of core's body types has a component in components/body: key-value, excerpt, prose, file-attachment (including stored File), item-list, image, component and media-object. A body type with no component draws nothing.

Prose displays plain text and unknown media types as escaped text. It parses Markdown with raw HTML and unsafe links disabled, and sanitizes rich HTML at render time using Symfony's HTML Sanitizer. Verbatim content is always escaped and preserves its source whitespace.

Styling

Blade uses the same starter-kit tokens as Vue. A starter-kit app already has these. For another Tailwind v4 app, add this minimal theme to app.css after @import "tailwindcss"; change the values to your application's palette:

@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
    --color-background: var(--background);
    --color-foreground: var(--foreground);
    --color-card: var(--card);
    --color-muted: var(--muted);
    --color-muted-foreground: var(--muted-foreground);
    --color-primary: var(--primary);
    --color-primary-foreground: var(--primary-foreground);
    --color-border: var(--border);
    --color-ring: var(--ring);
}
:root {
    --background: #fff;
    --foreground: #1f2933;
    --card: #fafafa;
    --muted: #f6f6f7;
    --muted-foreground: #6b7785;
    --primary: #1f2933;
    --primary-foreground: #fff;
    --border: #e2e5e9;
    --ring: #1f2933;
}
.dark {
    --background: #1b1b1f;
    --foreground: #dfdfd6;
    --card: #26262b;
    --muted: #202127;
    --muted-foreground: #98989f;
    --primary: #dfdfd6;
    --primary-foreground: #1b1b1f;
    --border: #3c3f44;
    --ring: #dfdfd6;
}

Add or remove the dark class on your layout to choose the theme. The kit is all Tailwind utilities; the avatar's snapshot colour and a picture's aspect ratio are data-driven inline styles. Verbatim prose keeps a dark code surface in both themes. Prose, lists and quotations carry their own utility styles.

Icon intents are application-defined strings exposed through data-sf-intent. To assign colours to your intent values, add the corresponding Tailwind utilities to the published components/glyph.blade.php view.

Rails, dividers and group state

<x-storyfeed::feed :page="$page" rail="actor"
    :dividers="[$timelineId => 'Timeline']" divider-style="branch" />

rail accepts actor (face + glyph badge), activity (glyph + face badge), actor-only and activity-only. The default matches Vue: actor-only, and activity-only for group children. A missing primary falls back to the other subject, then a blank disc; several actors suppress the badge. Avatars use data.avatar_color, then Vue's deterministic type/id palette; data.initials overrides initials. Tombstones suppress former pictures and colours. The default image fallback uses an inline error handler; apps with strict script CSP can supply an avatar renderer with their own fallback.

Day dividers are on by default; :grouped="false" hides them. Per-item dividers are keyed by public item id and work in either mode. divider-style="dot|branch" applies to both. timezone controls the display zone for days, the timestamp ladder and its absolute hover title. Timestamps are rendered on the server; the host owns any live refresh.

interactive and collapsed control groups independently. Native details works without JavaScript and supports keyboard disclosure. An unspecified state opens unnamed groups, or all groups when interactive is false. :collapsed="true" overrides that default; payload expanded still opens a group. With :interactive="false", no toggle is rendered and only the chosen server state is drawn. Groups show honest truncated-member counts and up to three sampled faces. Image bodies opt sampled objects into a linked media strip, hidden while the group's children are visible.

A raw JSON feed can also be rendered with :items="$payload['items']" and :next-cursor="$payload['next_cursor']"; page is optional on that path. The footer slot replaces the pager (for example, with a Livewire load-more control). divider, avatar, rail and media-strip are standalone components; media-strip accepts tiles, overflow, and a renderer callback.

Component bodies and host seams

Register an app Blade component in a service provider:

use Storyfeed\Ui\Support\BodyComponents;

app(BodyComponents::class)->register('App/Message', 'feed.message');

Storyfeed/Body/Component with name: "App/Message" renders <x-feed.message> using its props as typed component props. Stored names cannot choose arbitrary views: unregistered names draw nothing. The mapped component must exist. Regular custom body types remain supported as below. Forms are read from activity data, the object's body slot and object data, with data walking bounded to four levels like Vue; other roles' bodies are left to the app. Empty and unknown forms produce no wrapper.

Standalone activity/group components accept their default body slot, time and annotations slots, a removed text prop, and an object-icon image prop that frames the content stack. Object icons keep the object's URL and scalar link attributes, excluding href, event handlers and invalid names; they also pass through the media renderer. A missing URL or tombstone leaves the icon unlinked. Static groups (interactive=false) retain collapsed members with hidden print:block, while interactive groups keep native details print rules. For a whole feed, renderers propagates trusted application callbacks to every row and group child:

Key Callback receives Returns
time FeedItem timestamp/permalink HTML (including any refresh attributes)
glyph token, disc or badge icon SVG/HTML inside the kit's disc
avatar Entity, md, sm or badge complete avatar HTML
body, annotations FeedItem app content HTML
removed FeedItem optional escaped removal text
objectIcon FeedItem optional image array for the content frame
fileLabel {name, mediaType} array optional file-kind label; null uses the built-in MIME map
form body array, owning Entity or null body HTML; null uses the built-in renderer
mediaTiles, mediaOverflow group FeedItem replacement sample tiles or overflow count
media {image, href, attributes} tile array, Tailwind class string complete picture/tile HTML, including an optional lightbox

Callbacks are trusted application code and their HTML is not sanitized; stored payloads never supply callbacks. Body text itself is escaped or sanitized. Filament can use these seams for its icons, timestamp refresh and lightbox (the media callback reaches Image and MediaObject bodies as well as sample tiles; form can override an entire body). Standalone MediaObject also accepts image-placement="beside|below"; beside is the default in both kits. FileAttachment always shows its supplied name and formats decimal byte sizes (21 MB, 76 KB). Built-in MIME labels include PDF, images, Word and CSV (Spreadsheet (CSV)). The fileLabel callback overrides those labels, with null falling back to the MIME map and then the supplied MIME string. The standalone file component accepts labeller; <x-storyfeed::body> accepts file-labeller. No extension-based guessing or payload changes occur.

Feeds, items and groups accept child-rail="activity-only" independently of rail="actor", so expanded members can use glyph discs while their parent shows an actor and activity badge. Without a child override, children inherit the parent posture and dense rows suppress its badge.

Groups use explicit pinned singular slots and their own headline/template. Summary rendering has been removed from all three kits, mirroring core. Unknown extra payload keys are ignored. Sample photograph strips read Image bodies across every role, objects first, deduplicate image sources, cap at three and hide when children are shown. The Filament inventory lists the rendering boundary and the integration features that stay in the plugin.

Customising the Views

Publish the views to change the markup:

php artisan vendor:publish --tag=storyfeed-views

They land in resources/views/vendor/storyfeed, and a view there replaces the package's. You only need to keep the files you change.

Icons. The default Blade kit ships only its generic activity fallback. The payload's glyph is a token, such as shopping-bag, and the kit ships no icon set. Draw a token by adding resources/views/vendor/storyfeed/icons/shopping-bag.blade.php. A token with no view draws icons/activity.

Your own body types. A body type draws the component named after it: Acme/Attachment draws resources/views/vendor/storyfeed/components/body/acme/attachment.blade.php, which receives the body as $body and its entity as $entity.

Words. Headline words such as "Someone" and "a removed order" are core's translation lines (php artisan vendor:publish --tag=storyfeed-translations). The kit's own words, such as "Older activity" and "Show all :count", are plain __() strings: translate them in your lang/{locale}.json.

Licence

MIT. See LICENSE.md.

The paperclip icon is from Heroicons, copyright Tailwind Labs, Inc., used under the MIT licence. The full notice is included in licenses/heroicons.txt. The attachment row layout is original work.