feat: skin engine + Twenty CRM theme for iistwin
- Add client/src/themes/ with tokens, built-in skins (iistwin, twenty, omegaprom) - Add ThemeProvider with data-skin/data-theme and runtime CSS for custom skins - Add npm run skins:generate script for build-time CSS generation - Add organization_skins table, migration and CRUD API - Add activeSkinId to organizations and expose it in /api/auth/me - Add skin switcher in /profile and admin skin editor in /settings/appearance - Adapt index.css hardcoded colors to skin tokens - Update AGENTS.md and Obsidian notes
This commit is contained in:
@@ -32,12 +32,33 @@ export const organizations = pgTable("organizations", {
|
||||
displayName: varchar("display_name", { length: 200 }).notNull(),
|
||||
logoUrl: varchar("logo_url", { length: 500 }),
|
||||
themeColor: varchar("theme_color", { length: 7 }).default("#1976d2"),
|
||||
activeSkinId: varchar("active_skin_id", { length: 50 }).default("iistwin"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
isActive: boolean("is_active").default(true),
|
||||
billingBlocked: boolean("billing_blocked").default(false),
|
||||
});
|
||||
|
||||
// Organization skins
|
||||
export const organizationSkins = pgTable("organization_skins", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
skinId: varchar("skin_id", { length: 50 }).notNull(),
|
||||
name: varchar("name", { length: 100 }).notNull(),
|
||||
description: text("description"),
|
||||
tokens: jsonb("tokens").notNull(),
|
||||
isEditable: boolean("is_editable").default(true),
|
||||
isActive: boolean("is_active").default(false),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
uniqueOrgSkin: unique().on(table.organizationId, table.skinId),
|
||||
}));
|
||||
|
||||
export type OrganizationSkin = typeof organizationSkins.$inferSelect;
|
||||
export type InsertOrganizationSkin = typeof organizationSkins.$inferInsert;
|
||||
|
||||
// Billing settings per organization (1:1)
|
||||
export const organizationBilling = pgTable("organization_billing", {
|
||||
organizationId: integer("organization_id").primaryKey().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
@@ -678,6 +699,18 @@ export const messageReads = pgTable("message_reads", {
|
||||
export const organizationsRelations = relations(organizations, ({ many }) => ({
|
||||
users: many(users),
|
||||
forms: many(forms),
|
||||
skins: many(organizationSkins),
|
||||
}));
|
||||
|
||||
export const organizationSkinsRelations = relations(organizationSkins, ({ one }) => ({
|
||||
organization: one(organizations, {
|
||||
fields: [organizationSkins.organizationId],
|
||||
references: [organizations.id],
|
||||
}),
|
||||
createdByUser: one(users, {
|
||||
fields: [organizationSkins.createdBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
|
||||
Reference in New Issue
Block a user